藏川线前段

--- 摄于 2017 年 9 月 藏川线前段

最近一周,手上任务是继续 NAT 穿透工作的研究,并尽快整理好文档和方案,然后着手代码的实现上,属于近期的主要任务。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 模拟环境,并且在真实环境中也测试过,成功率其实不低,有兴趣的朋友可以玩一下。

这个过程中碰到的一些问题和看到的一些协议、代码实现,可以简单和大家分享一下。

udp2raw

注意到这个项目是因为有人说了 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 的关键部分。

UDPspeeder

对这个实现主要好奇的地方是 FEC(Forward Error Correction),通过重复发包率来实现纠错/对抗丢包,也是一种基于 udp 的可靠流实现了,虽然会对带宽造成一定量的浪费,但可以自己手动调整冗余率,也算是不错的东西了,而且这个实现还直接代理 tcp/udp/icmp 流量,工具箱又多了一件东西。

kcp

这也是一个基于 udp 的可靠流实现,同时还有多个语言实现版本:

go 的实现在原版的流量重传、窗口管理基础上,添加了 EFC、加密等功能,虽然可以手动关掉以保持兼容,但其实已经复杂了一大截了。同样,也是有一定的带宽浪费,同时还需要自己手动调整 mtu,默认是 1400,实在有点高,可能会被很多机器直接丢。

QUIC

这个协议主要是看了一下 rust 的两个实现:

quinn 在 API 上相对来说开放一点,它可以直接传入一个已经建立连接的 udp socket,然后开始 quic 协议构建,而 s2n_quic 并没有发现这样的接口。已经建立连接的 udp socket 意味着什么:可以将 NAT 穿透成功后的 udp socket 无缝转向一个可靠流协议实现,kcp 的实现也是支持这么做的。

但我比较讨厌 QUIC 在协议层强绑定 TLS 的做法,TLS 是一个很重的东西,需要巨大的基建工作,包括 dns 域名系统,证书发放和验证工作。如果 QUIC 单纯是一个基于 udp 的可靠流协议就好了,TLS 只是作为 Web 端的一层在 QUIC 之上,如果是这样的架构,QUIC 对我的吸引力就会更大了。

别人的 NAT 穿透实现

搜过之后会发现,网络编程相关,大部分是 c/cpp 的实现,这时候就需要稍微能看懂这些代码,不然就没有看源码的意义了,搜 github 第一个是 https://github.com/jflyup/nat_traversal 这个库。当你查看它核心代码实现的时候会发现以下几个功能:

turn

群友的 stun + turn 实现,几年前虽然读过 RFC,但现在早忘干净了。直接看代码瞅瞅它到底干了啥,主要是 stun 相关部分,毕竟咱只关心穿透,不关心中继,因为场景受限,只需要实现穿透相关就好,fallback 不需要实现。

AI 相关部分

在整个过程中,也少不了 AI 的帮助,尤其是 docker compose 配置和 iptables 设置的过程,大部分工作都是 AI 完成的,当然,也有 AI 无法理解的地方,需要自己手工填充,但是,无论怎么说,AI 真的是帮了大忙,很大程度上缩减了我消耗的时间,提高了效率。

网络拓扑图

一开始是想让 AI 直接生成图片,然后发现 AI 免费生成的都不咋地。经群友提示,让 AI 生成 plantuml 然后再渲染,这时候就要吐槽了,deepseek 一直给我错误语法的 plantuml,最后是手动修改成正确的语法,在线渲染之后发现颜值不高;再次经群友提示,换成了 mermaid,同样是在线生成,支持 css 语法。

我要特别夸一下 claude 的 mermaid 生成能力,demo 中的 mermaid 网络拓扑图完全是它根据 docker compose 配置文件生成的,第一遍生成不好看,让它美化一下就完成了,如果我自己手画,应该不会有这样的颜值的,真是太棒了。

Docker 的坑

这是一个很简单的 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 镜像的坑

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

问了好几遍,让它自己纠错,最后确实给了个对的。

iptables 与 route 与 docker 联手的坑

我要构建一个能模拟 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 工具,极大提升了生产力。

评论区

加载更多

登录后评论