--- 摄于 2017 年 9 月 藏川线前段
最近一周,手上任务是继续 NAT 穿透工作的研究,并尽快整理好文档和方案,然后着手代码的实现上,属于近期的主要任务。NAT 穿透实际上早在五年前就略微看过一点,但当时并没有着手去实现的紧急性和必要性,大致了解了概念和做法,简单尝试了一下就结束了。实际上当初简单尝试的产物并没有转化为真正有效的代码,或者说有效性并不高,这次尝试算是更深入的研究和实现。
简单讲背景就是 ipv4 下的互联网,由于地址数量有限,需要对整个公网地址分配做出限制,将一个大的网变成了 n 个小的局域网,局域网下又有局域网,每个局域网与外网的交互依赖于连接两个网络的 NAT 设备的转发流量。它会将内网地址映射成外网地址,并分配一个端口用来接收外网的响应,同时将接到的外网响应的目的地地址改成对应的内网地址,然后转发给内网设备。
这个分配端口的策略是不固定的,有很多种,包括复用端口,随机分配等等。在正常转发的同时,NAT 设备还会阻止未知的流量进入内网,一般而言,内网设备只有先发起连接/包,NAT 设备才会将外网的响应转发给内网,外网是无法直接直连内网设备的。
所谓的 NAT 穿透,最简单直接的场景就是:分别处于不同内网的两个设备想要进行直连,这时候就需要 NAT 穿透技术来实现,当然这种技术并不会一定有用,它取决于两台设备之间的网络情况、NAT 设备策略等等。而描述这门技术最全的文档要属 tailscale 的这篇文章,基本上所有需要处理的问题都有涉及,只要一一实现,就能达到 tailscale 目前的体验(不是)。
但是,它只描述了基于 udp 协议的穿透问题,tcp 协议只是提了一下:“比 udp 更复杂也更难”。tcp 的问题在于它是一个基于连接状态的流式协议,并且直接写在内核中,比较难操作,但不是不行,可以参考的文献有:
还可以通过 AI 搜索论文,这方面的资料虽然不多,但也不少,基本内容都有涵盖。tcp 协议在穿透的时候,核心要点就是:
udp 协议无握手过程,直接发包就行,收到对方的包之后就真的通了,相对来说比较简单。
整个研究过程到现在基本算告一段落了,流程从 tcp 相关资料到 udp 相关资料,然后到基于 udp 的协议实现上,看了不少内容,并且针对这些实现,写了一个 tcp 和 udp 穿透的 demo 实现,且有配套的 docker-compose 模拟环境,并且在真实环境中也测试过,成功率其实不低,有兴趣的朋友可以玩一下。
这个过程中碰到的一些问题和看到的一些协议、代码实现,可以简单和大家分享一下。
注意到这个项目是因为有人说了 udpspeeder,顺带就看了看 udp2raw,两者配合能做出不少事情。udp2raw 是将正常的 udp 包伪装成 tcp 流来绕开运营商对 udp 协议不友好的丢包对待,从而实现稳定的 udp 通信。看到这个库的时候,我其实不关心它是怎么代理 udp 的,我关心的问题是,它是怎么伪装的,然后我在代码中看到了这一段:
raw_send_fd = socket(PF_PACKET, SOCK_DGRAM, htons(ETH_P_IP));
...
raw_recv_fd = socket(PF_PACKET, SOCK_DGRAM, htons(ETH_P_IP));
它的 server 和 client 在通信上是用的一对 raw socket,然后手工将 udp 包加上 tcp header + ip header 然后发出去,对端收到之后也一样需要手工拆开这些东西,最后得到一个正常的 udp 消息。然后我就理解了,它的程序运行为什么需要 root 权限,同时需要 iptables 设置特殊的 filter 规则用来接收 raw socket 的消息,因为系统根本不知道这东西是给它的(没有 tcp 握手过程,系统不会自动分发),需要手工操作。使用 raw socket 能完成很多特权操作,能节流一些本不属于自己的消息流,也是安全相关的部分,同时它也是手搓模拟 tcp 的关键部分。
对这个实现主要好奇的地方是 FEC(Forward Error Correction),通过重复发包率来实现纠错/对抗丢包,也是一种基于 udp 的可靠流实现了,虽然会对带宽造成一定量的浪费,但可以自己手动调整冗余率,也算是不错的东西了,而且这个实现还直接代理 tcp/udp/icmp 流量,工具箱又多了一件东西。
这也是一个基于 udp 的可靠流实现,同时还有多个语言实现版本:
go 的实现在原版的流量重传、窗口管理基础上,添加了 EFC、加密等功能,虽然可以手动关掉以保持兼容,但其实已经复杂了一大截了。同样,也是有一定的带宽浪费,同时还需要自己手动调整 mtu,默认是 1400,实在有点高,可能会被很多机器直接丢。
这个协议主要是看了一下 rust 的两个实现:
quinn 在 API 上相对来说开放一点,它可以直接传入一个已经建立连接的 udp socket,然后开始 quic 协议构建,而 s2n_quic 并没有发现这样的接口。已经建立连接的 udp socket 意味着什么:可以将 NAT 穿透成功后的 udp socket 无缝转向一个可靠流协议实现,kcp 的实现也是支持这么做的。
但我比较讨厌 QUIC 在协议层强绑定 TLS 的做法,TLS 是一个很重的东西,需要巨大的基建工作,包括 dns 域名系统,证书发放和验证工作。如果 QUIC 单纯是一个基于 udp 的可靠流协议就好了,TLS 只是作为 Web 端的一层在 QUIC 之上,如果是这样的架构,QUIC 对我的吸引力就会更大了。
搜过之后会发现,网络编程相关,大部分是 c/cpp 的实现,这时候就需要稍微能看懂这些代码,不然就没有看源码的意义了,搜 github 第一个是 https://github.com/jflyup/nat_traversal 这个库。当你查看它核心代码实现的时候会发现以下几个功能:
群友的 stun + turn 实现,几年前虽然读过 RFC,但现在早忘干净了。直接看代码瞅瞅它到底干了啥,主要是 stun 相关部分,毕竟咱只关心穿透,不关心中继,因为场景受限,只需要实现穿透相关就好,fallback 不需要实现。
在整个过程中,也少不了 AI 的帮助,尤其是 docker compose 配置和 iptables 设置的过程,大部分工作都是 AI 完成的,当然,也有 AI 无法理解的地方,需要自己手工填充,但是,无论怎么说,AI 真的是帮了大忙,很大程度上缩减了我消耗的时间,提高了效率。
一开始是想让 AI 直接生成图片,然后发现 AI 免费生成的都不咋地。经群友提示,让 AI 生成 plantuml 然后再渲染,这时候就要吐槽了,deepseek 一直给我错误语法的 plantuml,最后是手动修改成正确的语法,在线渲染之后发现颜值不高;再次经群友提示,换成了 mermaid,同样是在线生成,支持 css 语法。
我要特别夸一下 claude 的 mermaid 生成能力,demo 中的 mermaid 网络拓扑图完全是它根据 docker compose 配置文件生成的,第一遍生成不好看,让它美化一下就完成了,如果我自己手画,应该不会有这样的颜值的,真是太棒了。
这是一个很简单的 dockerfile 文件,没有什么特殊的,但它构建完成之后,无法工作,也没有报错信息。
FROM rust:1.85.0 as build
WORKDIR /usr/src/nat-traversal
COPY . .
RUN cd /usr/src/nat-traversal && cargo build --release
FROM debian:12
WORKDIR /app
COPY --from=build /usr/src/nat-traversal/target/release/nat_traversal /app/nat-traversal
CMD ["tail", "-f", "/dev/null"]
rust:1.85.0 这个镜像用的就是 debian 系统,但直接跑不了,换成构建的镜像就没问题了,严重怀疑是 glibc 的问题。而且这个写法在之前,至少去年应该是没问题的,实在是太坑了。
debian 在 10 开始,就从 iptables 切到了 nfttables,然后如果你用这种方式构建镜像:
FROM rust:1.85.0
RUN apt-get update && apt-get install nftables
进去之后,iptables 命令没有,nft 命令半残,直接无法使用,完全不知道怎么操作。
然后 deepseek 告诉我这么做,就可以用 iptables
FROM rust:1.85.0
RUN apt-get update && apt-get install -y sudo iptables && rm -rf /var/lib/apt/lists/* && update-alternatives --set iptables /usr/sbin/iptables-legacy
RUN echo 'user ALL=(root) NOPASSWD:/usr/sbin/iptables' >> /etc/sudoers
问了好几遍,让它自己纠错,最后确实给了个对的。
我要构建一个能模拟 nat 网络的 docker compose 环境,我问了好多个 AI,在众多 AI 的带领下,终于得到了一个配置,但不可用,期间有:deepseek r1、Qwen、Poe 上的 claude 3.7、OpenRouter 上的众多 AI,他们是尽力了,但确实不可用,这时候的状态:
peer1 和 peer2 虽然在 nat net 端无法通信,但 public net 上能直接互联。
然后我将它写成 yaml,问 vscode 上的 claude 3.7 thinking,经历过至少六七次修改,出了一个增强版,已经很接近完成了,但还是没完成,我将这份接近完成的配置丢给了 Gork3,它捣鼓了五六次,基本属于重复 claude 的操作,依然没有完成,这时候的状态是:
实际上就是要解决 gateway 的转发设置就行了,但是 Gork 与 claude 都纠结在如何写 compose 文件让它启动的时候构建完成配置,而实际上启动过程中 getent hosts nat1_gateway | awk '{print $1}'
这条命令是无输出的,找不到 gateway 对应的 ip,也就无法设置 peer 的 route,这也是问题的关键。
中间还碰到了直接写死 ip 段会在启动的时候报错 address already use
的错误,检查了电脑环境非常干净,就是报错,比较怀疑是 stun server 与 gateway 共享一个 ip 段造成的问题,最后选择了让 docker 自己分配 ip 段绕开这种问题。
之后我直接放弃了在 compose 中配置 route,让它启动完成之后再手动配置,这样环境就正常工作了。不过 iptables 的规则可以在 compose 中直接设置,这样也节省了不少操作,docker compose 的网卡创建顺序就是配置中的网卡顺序,定序的创建网卡也是 iptables 能直接操作的原因。
综上,群友帮助了不少内容,然后 AI 帮助了不少内容,尤其是网络拓扑图生成,太赞了。如果我一个人做这些收集整理 + 测试工作,感觉至少需要一倍以上的时间去完成,善用 AI 工具,极大提升了生产力。
请登录后评论
评论区
加载更多