又是一篇关于 UDP 打洞的文章。之前写过关于在完全圆锥型(full cone)NAT的文章中如何使用 socat 命令打洞。根据那篇文章里的知识,连接到一个 full cone NAT 后边的 mosh 不成问题。不过,我现在的网络是受限圆锥型(restricted cone)NAT 了呢!
也就是复杂了一些。双方要向中间服务器和对方都发送数据包才可以。另外就是,客户端(mosh-client)这边得使用在打洞期间使用的端口号才行。
打洞流程根据维基百科,双方通过中间服务器(还是我的 udpaddr 啦)交换地址,双方均向得到的地址发送一数据包,然后开始正常通讯。比较麻烦,于是有了这个脚本:
#!/usr/bin/env python3 import socket import re import sys import subprocess udp_server = ('xmpp.vim-cn.com', 2727) addr_re = re.compile(r"\('(?P<ip>[^']+)', (?P<port>\d+)(?:, (?P<cport>\d+))?") def main(server): sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) sock.settimeout(2) print('Send message...') sock.sendto(b'req from holepunch.py\n', udp_server) msg, addr = sock.recvfrom(1024) print('Got answer from %s: %s' % (addr, msg)) m = addr_re.search(msg.decode()) if not m: print("Error: can't parse answer.") sys.exit(1) m_ip = m.group('ip') m_port = int(m.group('port')) port = sock.getsockname()[1] print('Got my IP and Port: (%r, %s, %s).' % (m_ip, m_port, port)) msg = input('> Peer address: ') m = addr_re.search(msg) if not m: print("Error: can't parse input.") sys.exit(2) p_ip = m.group('ip') p_port = int(m.group('port')) c_port = int(m.group('cport')) print('send initial packet and wait for answer...') sock.sendto(b'HELO\n', (p_ip, p_port)) try: msg = sock.recvfrom(1024) print('Received:', msg) except socket.timeout: print("Timed out (it's normal).") if server: sock.close() print('Starting mosh server...') msg = subprocess.check_output(['mosh-server', 'new', '-p', str(port)]) secret = msg.split()[3].decode() print('Connect with:\nMOSH_KEY=%s MOSH_CPORT=%s mosh-client %s %s' % (secret, c_port, m_ip, m_port)) else: print('done.') if __name__ == '__main__': server = len(sys.argv) == 2 and sys.argv[1] == '-s' main(server)
如果 mosh 服务器端位于受限 NAT 后,还需要给 mosh-client 打个(我随手写的很 dirty 的)补丁以便指定客户端使用的 UDP 端口号:
diff --git a/src/network/network.cc b/src/network/network.cc index 2f4e0bf..718f6c5 100644 --- a/src/network/network.cc +++ b/src/network/network.cc @@ -176,6 +176,11 @@ Connection::Socket::Socket() perror( "setsockopt( IP_RECVTOS )" ); } #endif + + if ( getenv("MOSH_CPORT") ) { + int port = atoi(getenv("MOSH_CPORT")); + try_bind( _fd, INADDR_ANY, port, port); + } } void Connection::setup( void )
然后,连接流程如下:
-
mosh 服务端运行
holepunch.py -s
命令,客户端运行holepunch.py
; -
双方看到自己的地址信息(依次是
IP, 外网端口, 本地端口
)后,复制并发送给对方; - 双方输入对方的地址。为了节省时间(mosh-server 只会等一分钟,NAT 上的端口映射也是有时效的),只要输入的一行内包含上述地址信息的文本即可,前后可以有不小心复制过来的多余字符;
- 服务端将得到一行包含 mosh 的密钥的命令。将此命令发送对客户端;客户端运行此命令连接。如果 mosh-client 的名字不是标准名字,需要自行修改;
- 一切顺利的话就连接上啦!
mosh 服务端输出示例:
>>> holepunch.py -s Send message... Got answer from ('202.133.113.62', 2727): b"Your address is ('180.109.80.47', 3169)\n" Got my IP and Port: ('180.109.80.47', 3169, 8127). > Peer address: Port: ('222.95.148.73', 5223, 55473). send initial packet and wait for answer... Timed out (it's normal). Starting mosh server... mosh-server (mosh 1.2.4) Copyright 2012 Keith Winstein <mosh-devel@mit.edu> License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>. This is free software: you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law. [mosh-server detached, pid = 5202] Connect with: MOSH_KEY=1apguNqtfN/K4JSvllnJxA MOSH_CPORT=55473 mosh-client 222.95.148.73 5223
mosh 客户端输出示例:
>>> holepunch.py Send message... Got answer from ('202.133.113.62', 2727): b"Your address is ('222.95.148.73', 5223)\n" Got my IP and Port: ('222.95.148.73', 5223, 55473). > Peer address: rt: ('180.109.80.47', 3169, 8127) send initial packet and wait for answer... Timed out (it's normal). done.
2013年10月20日更新:想要通过打出来的 UDP 洞进行 TCP 通信吗?参见文章通过 OpenVPN 让 TCP 使用 UDP 洞。