IPv6 防火墙 TCP 打洞

 

某天我在使用 syncthing 同步时, 发现外网的主机 (称为B) 成功连接到了我家防火墙内的主机 (称为 A).

当时我检查了主机 A 所处网络的 OpenWRT 路由器的防火墙设置. 设置为 Input reject, Output accept, Forward reject.

理论上路由器不会允许外网主机 B 直接访问主机 A, 可它却连上了…

众所周知, 困扰程序员的两大问题就是它为什么能跑和它为什么不能跑.

于是我就打算来探究一下它为什么能跑…

Syncthing 连接Syncthing 连接

排查配置问题

再来详细确认排查一下各个配置问题.

OpenWRT 官方固件默认不支持 UPnP, 我也没有进行过相关配置.

OpenWRT 防火墙配置如图, 同时防火墙规则里也没有放行相关连接.

防火墙规则防火墙规则

Wireshark 抓包

Wireshark 抓包确认的确是由外网的主机 B 主动发起了 TCP 连接, 成功连接到了内网的 A 主机.

下图为主机 B 上的抓包数据:

抓包抓包

又在服务器 A 上进行抓包:

抓包抓包

发现一个特殊的地方, 服务器用于发起和接受 TCP 连接的端口是一样的. (22000)

查找相关资料

https://github.com/syncthing/syncthing/issues/4259 (这里给出了这种打洞方法的简易 PoC)

https://github.com/syncthing/syncthing/commit/f619a7f4ccd32ae853b3b84c0171fb5698b1370e

在 GitHub 的 issues 找到了相关的讨论, 找到了打洞的原理.

简而言之, 常见的家用路由器 (包括 OpenWRT) 都不会追踪 TCP state machine 的状态.

A 先从 22000 端口向主机 B 发起 TCP 连接, 发出 TCP SYN, 被路由器防火墙记录.

A 发出的 TCP SYN 由于 B 所在网络的设置被丢弃.

A 在 22000 端口监听 TCP 连接.

B 向 A 发起 TCP 连接, 发出 TCP SYN, 经过 A 所在网络的防火墙时, 由于路由器不区分 TCP SYN 和 TCP SYN+ACK 这两种数据包, 仅通过 ip + 端口号将 TCP SYN 误判为对首次连接的回复 (outbound stream), 放行.

A 收到 TCP SYN 后回复 SYN+ACK, B 收到后回复 ACK, 建立 TCP 连接, 继续 TLS 握手等步骤并 Keep alive, 打洞成功.

完成上述打洞过程要求 A 和 B 两台设备拥有独立的 ip 地址, 不能是 ipv4 NAT. 并且至少有一方的路由器防火墙没有追踪 TCP 状态(常见于家用路由器), 上述例子中, A 主机网络的 OpenWRT 符合条件, 而对于 B 所在网络的路由器, B 完成的是完全正常的 TCP 连接.

(实际场景是, A 为我家中的 HomeLab, B 为我在大学内网的笔记本, 大学网络路由器的防火墙较为严格, 打洞失败.)

解释相关原理

综上, 可以解释 Syncthing 的工作原理:

  1. 手动交换两台机器的识别码后, 通过公开的 relay 交换两台机器的 ip 地址.
  2. A 复用端口, 向 B 发起连接的同时并监听.
  3. B 向 A 发起连接, 连接成功.

本文采用 CC BY-NC-SA 4.0 许可协议发布.

作者: lyc8503, 文章链接: https://blog.lyc8503.net/post/ipv6-tcp-hole-punching/
如果本文给你带来了帮助或让你觉得有趣, 可以考虑赞助我¬_¬