内网 P2P 隧道打洞方案
分享下我的内网 P2P 隧道打洞方案,适用于一些网络环境限制不是异常严格的内网环境。
我的需求场景
公司的内网环境比较复杂,存在 NAT 和防火墙等限制,安防软件会定期覆盖公司电脑的路由防火墙配置。
我的使用场景:
- 使用自己的电脑进行项目开发,不打算使用公司的电脑
- 某些场景隧道到公司内网访问一些内网资源(如数据库、测试环境等)
P2P 隧道打洞
两台都在 NAT 后面的机器谁也没有公网 IP,互相都连不上对方。传统做法是找一台有公网 IP 的服务器做中转(frp、VPN 都是这个思路),所有流量绕服务器走一圈,延迟和带宽都被这台中转机卡住。
P2P 打洞换了一个思路:公网服务器只负责牵线,不转发数据。两端先各自连上协调服务器,服务器把双方的公网出口地址和端口互相告知,然后两端同时向对方的出口地址发包。NAT 设备看到出站流量,会在映射表里加一条内网端口到公网端口的映射,对方的包到达时正好命中这条映射,于是被放行。两边的洞都打通后,数据直接在两台机器之间跑,协调服务器退出数据路径。
可以用现有的三方服务 Tailscale、ZeroTier,我用的是 Tailscale,直接两边起服务登账号就可以用。
- 底层隧道用 WireGuard,节点之间的流量端到端加密,协调服务器只交换公钥和端点信息,碰不到数据
- 每台设备分配一个 100.x.y.z 的虚拟 IP(CGNAT 保留段),设备换网络这个 IP 也不变
- 打洞成功走直连;打不通时自动回落到 DERP 中继服务器,保证连通,只是延迟高一些
对上层应用来说,这就是一个普通的 IP 网络,SSH、Syncthing 直接填 100.x 的地址就能用。比如我的 ssh 配置:
| |
ServerAliveInterval 除了防 ssh 断连,还有保活的作用。NAT 上的映射条目有超时时间,长时间没有流量会被回收,定期心跳能让洞一直开着。
NAT 穿透
打洞能不能成,取决于两端 NAT 的行为。NAT 的基本工作方式:内网机器发出站包时,网关把内网的 IP:端口 翻译成公网的 IP:端口 并记进映射表,回包按表翻译回去;没有命中映射的入站包直接丢弃。穿透的关键就是让两端提前在各自的 NAT 上建好映射。
NAT 按映射行为大致分两类,对打洞难度影响很大:
- 锥型(Full Cone / Restricted Cone / Port Restricted Cone):同一个内网端口不管发给谁,对外都用同一个公网端口。出口端口可预测,打洞基本都能成
- 对称型(Symmetric):每换一个目标地址就分配一个新的公网端口。对方拿到的端口信息在真正发包时已经失效,几乎打不通
实际的 UDP 打洞流程分三步:
- 两端分别向公网的 STUN 服务器发包,从响应里得知自己的公网出口 IP 和端口
- 通过协调服务器交换这个信息
- 两端同时向对方的出口地址发 UDP 包。第一个包大概率被对方 NAT 丢掉,但它在自己这侧打出了映射;对方的包随后到达,命中映射进来,链路建立
tailscale 打洞
tailscale 使用非常简单,双方电脑都安装 tailscale 客户端,注册同一个账号登录后基本就能互相访问了。
像公司内网这种环境,常见的阻碍是对称型 NAT 加上防火墙限制 UDP。Tailscale 会尝试 IPv6 直连、用 UPnP/NAT-PMP 向路由器申请端口映射,对于对称型 NAT 会批量猜测端口提高命中概率。如果还是打不通就走 DERP 中继兜底,所以体验上总能连上,区别只是直连用着最舒服、中继延迟比较明显。
常用的 tailscale 命令:
| |
开发项目
P2P 隧道通了之后,后续开发流程又优化了几次,下面是体验过的几个项目同步方案。
方案一:vscode remote 连 tailscale 隧道 IP
最直接的用法,vscode ssh 走 tailscale 的隧道 IP 远程开发。
用下来延迟比较高,也不太稳定,比如 tailscale 本来是直连,网络变化导致打洞失败掉到中继,vscode 可能就断了。开发 go 代码每次保存还要等 go fmt 格式化几秒钟,体感很差。
方案二:syncthing 同步代码
后来改成用 syncthing 同步代码,vscode 编辑本地的同步副本,再 ssh 到公司内网的机器上提交代码。编辑这一侧没有延迟问题了,但提交代码还要绕一道远程机器,现在回头看非常多此一举。
方案三:单独给 git 配置 ssh socks 代理
现在的做法:vscode 直接编辑本地文件,只给 git 配一个 ssh socks 代理,push/pull 的时候走代理通过 tailscale 隧道访问公司内网的 git 服务器。大部分时间纯本地开发,需要访问公司内网资源时才走代理,平时不受网络环境变化的影响,是目前开发体感最好的方案。
操作步骤:
- 两边的电脑保持 tailscale 连接,确认能互相访问
- 本地电脑起一个 ssh socks 代理,监听本地一个端口(比如 1080),配置文件示例:
| |
- 项目的 .git 配置 http.proxy 或 https.proxy 指向这个 socks 代理,比如
http.proxy socks5h://127.0.0.1:1080
socks5h 协议,h 代表 hostname,表示让代理服务器解析域名而不是本地解析。因为本地解析不了公司内网的域名,所以必须让代理服务器来解析。