昨天在博客里写道要学习下 Tailscale, 今天简单试验一下 Tailscale 博客文章 How NAT traversal works 里面的最简单情况:UDP 通信在已知对方 IP 和 端口的情况下,穿越对方的状态防火墙
原理
原理方面上述的文章已经介绍得非常清楚了,尤其图片画得很清晰,我根据我的理解描述下。
通常情况下,状态防火墙(stateful firewall) 会对入站流量进行检测,只有在最近一段时间内曾做过目的地址的 (ip, port) 发来的流量才会被接受。
于是就有了这个最简单的穿越状态防火墙的例子,即双方互相知道对方的 IP 的端口:
- 假设 A (2.2.2.2, 12345) 与 B (3.3.3.3, 54321) 需要通信,双方提前约好
- A 给 B 发了一个包,显然,肯定会被 B 的防火墙阻止,但是 B 的信息会被 A 的防火墙临时记录下来
- B 给 A 回复一个包,这时,A 的防火墙根据当前状态会选择接受这个包(这也是显然的,不然 A 就没法上网了)。另外,A 的信息这时也会被 B 的防火墙记录
- 于是,在这之后,A 给 B 发的包也会被 B 的防火墙接受。双向通信建立起来了。
啊,没有 NAT 的网络多么美好。
验证
于是用两个有公网 IPv6 地址的设备进行了小实验复现这个过程,双方都开启了 ufw 防火墙,平时无法直接访问到它的 UDP 端口。
只需要在两台机器上几乎同时地运行以下脚本,就能看到双方可以正常通信了:
import sys
import time
import socket
PORT_I = 12345
PORT_R = 54321
ADDR_I = ("<initiator address>", PORT_I)
ADDR_R = ("<responder address>", PORT_R)
# initiator
def initiator():
sock = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
sock.bind(ADDR_I)
sock.sendto(b"Ping", ADDR_R)
while data := sock.recvfrom(1024):
print(f"recv: {data[0]} from {data[1]}")
time.sleep(1)
sock.sendto(b"a"*10, ADDR_R)
# responder
def responder():
sock = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
sock.bind(ADDR_R)
sock.sendto(b"Pong", ADDR_I)
while data := sock.recvfrom(1024):
print(f"recv: {data[0]} from {data[1]}")
time.sleep(1)
sock.sendto(b"b"*10, ADDR_I)
if __name__ == '__main__':
if len(sys.argv) != 2:
print("Invalid args")
if sys.argv[1] == "1":
initiator()
elif sys.argv[1] == "2":
responder()
但是这种情况过于理想,现实中收数据的一方必须几乎同时作出反应才行,这需要它提前知道对方要给自己发消息。这也很容易验证,A 先运行脚本之后,B 等上一小会再运行,发现这次就不能通信了。
因此,还需要一个”侧信道“,即一个双方都能访问的协调用的服务器,通知接收方及时回复消息。
总结
水了一篇博客,演示了一个最简单的 UDP 通信穿越状态防火墙的例子, 下一步计划是实现一个简单的用于协调双方的服务器。