RFC5389 关于 STUN(Session Traversal Utilities for NAT,NAT会话穿越应用程序)的详细描述中定义了一个协议回答了上面的一部分问题,这是一篇内容很长的 RFC,所以我将尽我所能对其进行总结。先提醒一下,STUN 并不能直接解决上面的问题,它只是个扳手,你还得拿他去打造一个称手的工具:
STUN 本身并不是 NAT 穿透问题的解决方案,它只是定义了一个机制,你可以用这个机制来组建实际的解决方案。
—
STUN(Session Traversal Utilities for NAT,NAT会话穿越应用程序)是一种网络协议,它允许位于NAT(或多重NAT)后的客户端找出自己的公网地址,查出自己位于哪种类型的 NAT 之后以及 NAT 为某一个本地端口所绑定的公网端口。这些信息被用来在两个同时处于 NAT 路由器之后的主机之间建立 UDP 通信。该协议由 RFC 5389 定义。
STUN 是一个客户端-服务端协议,在上图的例子中,Alice 是客户端,Carol 是服务端。Alice 向 Carol 发送一个 STUN Binding 请求,当 Binding 请求通过 Alice 的 NAT 时,源 IP:Port 会被重写。当 Carol 收到 Binding 请求后,会将三层和四层的源 IP:Port 复制到 Binding 响应的有效载荷中,并将其发送给 Alice。Binding 响应通过 Alice 的 NAT 转发到内网的 Alice,此时的目标 IP:Port 被重写成了内网地址,但有效载荷保持不变。Alice 收到 Binding 响应后,就会意识到这个 Socket 的公网 IP:Port 是 2.2.2.2:7777。
然而,STUN 并不是一个完整的解决方案,它只是提供了这么一种机制,让应用程序获取到它的公网 IP:Port,但 STUN 并没有提供具体的方法来向相关方向发出信号。如果要重头编写一个具有 NAT 穿透功能的应用,肯定要利用 STUN 来实现。当然,明智的做法是不修改 WireGuard 的源码,最好是借鉴 STUN 的概念来实现。总之,不管如何,都需要一个拥有静态公网地址的主机来充当信使服务器。
5. NAT 穿透示例早在 2016 年 8 月份,WireGuard 的创建者就在 WireGuard 邮件列表上分享了一个 NAT 穿透示例。Jason 的示例包含了客户端应用和服务端应用,其中客户端应用于 WireGuard 一起运行,服务端运行在拥有静态地址的主机上用来发现各个 Peer 的 IP:Port,客户端使用原始套接字(raw socket)与服务端进行通信。
/* We use raw sockets so that the WireGuard interface can actually own the real socket. */ sock = socket(AF_INET, SOCK_RAW, IPPROTO_UDP); if (sock < 0) { perror("socket"); return errno; }正如评论中指出的,WireGuard 拥有“真正的套接字”。通过使用原始套接字(raw socket),客户端能够向服务端伪装本地 WireGuard 的源端口,这样就确保了在服务端返回响应经过 NAT 时目标 IP:Port 会被映射到 WireGuard 套接字上。
客户端在其原始套接字上使用一个经典的 BPF 过滤器来过滤服务端发往 WireGuard 端口的回复。
static void apply_bpf(int sock, uint16_t port, uint32_t ip) { struct sock_filter filter[] = { BPF_STMT(BPF_LD + BPF_W + BPF_ABS, 12 /* src ip */), BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, ip, 0, 5), BPF_STMT(BPF_LD + BPF_H + BPF_ABS, 20 /* src port */), BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, PORT, 0, 3), BPF_STMT(BPF_LD + BPF_H + BPF_ABS, 22 /* dst port */), BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, port, 0, 1), BPF_STMT(BPF_RET + BPF_K, -1), BPF_STMT(BPF_RET + BPF_K, 0) }; struct sock_fprog filter_prog = { .len = sizeof(filter) / sizeof(filter[0]), .filter = filter }; if (setsockopt(sock, SOL_SOCKET, SO_ATTACH_FILTER, &filter_prog, sizeof(filter_prog)) < 0) { perror("setsockopt(bpf)"); exit(errno); } }客户端与服务端的通信数据都被定义在 packet 和 reply 这两个结构体中:
struct { struct udphdr udp; uint8_t my_pubkey[32]; uint8_t their_pubkey[32]; } __attribute__((packed)) packet = { .udp = { .len = htons(sizeof(packet)), .dest = htons(PORT) } }; struct { struct iphdr iphdr; struct udphdr udp; uint32_t ip; uint16_t port; } __attribute__((packed)) reply;