又是一篇关于 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 洞。