从「在外面怎么访问家里电脑」聊起:NAT 穿透、UDP 打洞与 WireGuard
- 从「在外面怎么访问家里电脑」聊起:NAT 穿透、UDP 打洞与 WireGuard
从「在外面怎么访问家里电脑」聊起:NAT 穿透、UDP 打洞与 WireGuard
一篇从问题出发,层层深入的技术科普
一、一个真实的问题
你有没有遇到过这样的场景:
- 在公司想 SSH 回家里的电脑
- 出门在外想访问家里的 NAS
- 想远程桌面到家里的工作站
按理说,互联网应该能让任何两台设备互相通信。但当你尝试从手机直接连接家里电脑时,发现根本连不上。
为什么?
二、为什么这么难?—— NAT 的前世今生
IPv4 地址不够用了
互联网最初设计时,IPv4 地址有 43 亿个,当时觉得足够用了。但随着设备爆炸式增长,地址早就不够分了。
解决方案?NAT(Network Address Translation,网络地址转换)。
NAT 的工作原理
你家里可能有十几台设备:手机、电脑、平板、智能音箱……但你的运营商只给你分配了一个公网 IP。
NAT 让这成为可能:
家里的设备 家庭路由器 公网
(NAT)
手机 192.168.1.100 ──┐
电脑 192.168.1.101 ──┼──► NAT 转换 ──► 123.45.67.89(公网 IP)
平板 192.168.1.102 ──┘
当你电脑访问外网时:
- 电脑发包:
192.168.1.101:12345 → 目标服务器 - 路由器 NAT 转换:
123.45.67.89:54321 → 目标服务器 - 路由器记住:
54321 端口 = 电脑的 12345 端口 - 服务器回复到
123.45.67.89:54321 - 路由器查表,转发给
192.168.1.101:12345
核心问题:内网设备没有「真正的公网身份」
这就是问题所在:
| 你想做的 | 现实情况 |
|---|---|
连接 192.168.1.101 |
这个地址在公网不存在 |
连接 123.45.67.89 |
路由器不知道转发给谁 |
NAT 映射有两个致命特点:
- 临时的 —— 几分钟没用就过期删除
- 单向的 —— 只有内网主动发起才会建立映射,外部无法主动连入
这就是为什么你在外面无法直接访问家里电脑。
三、打洞:如何骗过 NAT
既然 NAT 是问题根源,那我们就想办法「骗过」它。
为什么用 UDP 不用 TCP
TCP 需要三次握手建立连接:
A → B: SYN
B → A: SYN-ACK
A → B: ACK
问题是,如果 A 和 B 都在 NAT 后面,第一个 SYN 包就会被对方的 NAT 丢弃——因为对方 NAT 没有对应的映射。
为什么会丢弃?因为 NAT 映射只能由内网主动发包建立。
可以把 NAT 想象成一个 只认「出门条」的门卫:
内网设备发包出去时:
内网 A → NAT → 公网
│
└─ NAT 记下:"A 出门了,去找 X,走的是 5000 号门"
(这就是建立「映射」)
外部回包时:
公网 → NAT → 内网 A
│
└─ NAT 查表:"5000 号门是 A 出去的,放行"
外部主动发包时(没有对应的出门记录):
公网 → NAT → ???
│
└─ NAT 查表:"没人从这个门出去过,不知道给谁,丢弃"
NAT 不会「猜」应该转发给谁——内网可能有几十台设备,它怎么知道这个包是给谁的?所以默认策略就是 丢弃。
回到 TCP 打洞的问题:
A(内网) A 的 NAT B 的 NAT B(内网)
192.168.1.2 1.1.1.1 2.2.2.2 192.168.1.3
│ │ │ │
├─── SYN ───────────►├─────── SYN ─────────►│ │
│ │ │ │
│ A 的 NAT 建立映射: B 的 NAT 收到包,
│ "1.1.1.1:5000 ↔ 内网 A" 但映射表里没有
│ "来自 1.1.1.1 的包该转给谁"
│ │
│ ▼
│ 丢弃!
B 的 NAT 不知道该把这个包转发给内网的哪台设备——因为 B 从来没有主动联系过 A,NAT 里没有对应的映射记录。NAT 的逻辑是:「我只放行我认识的流量」,而「认识」的前提是内网设备先发过包给对方。
UDP 不一样,它是「无连接」的——发出去就完事,不需要对方先回应。这给了我们操作空间。
等等,UDP 的第一个包不也会被丢弃吗?是的,但 UDP 不在乎。
TCP 的问题:
A 发 SYN → 被丢弃 → A 等 SYN-ACK → 超时 → 连接失败
(TCP 必须收到回复才能继续)
UDP 的优势:
A 发包 → 被丢弃 → A:「无所谓,我又不等回复」
│
└─ 但重点是:A 的 NAT 已经建立映射了!
UDP 打洞的精髓在于:我不指望第一个包能到,我只需要它在我的 NAT 上「开个门」。
只要 A 和 B 同时往对方发包,双方的 NAT 都会建立映射。然后下一轮包就能互相通过了——因为对方的 NAT 上已经有「出门记录」了。
那 TCP 不能也这样吗?
理论上可以。TCP 协议支持「同时打开」(simultaneous open)——双方同时发 SYN,然后各自回 SYN-ACK。但实际操作有几个麻烦:
| 问题 | 说明 |
|---|---|
| 时机要求苛刻 | TCP 发完 SYN 就进入等待状态,必须双方精确同时发起,窗口期很短 |
| NAT 可能发 RST | 有些 NAT 收到「不认识」的 SYN 不只是丢弃,还会回复 RST(重置),直接打断你的连接尝试 |
| 操作系统支持差 | 很多系统的 connect() 实现对 simultaneous open 支持不好 |
| 应用层不友好 | 标准的 socket API 不太方便实现这种「同时连接」的模式 |
完整对比一下两者的打洞流程:
UDP 打洞:
第 1 轮:
A 发包给 B → A 开门 ✓ → 包到 B 的 NAT → B 没开门 → 丢弃
B 发包给 A → B 开门 ✓ → 包到 A 的 NAT → A 没开门 → 丢弃
(虽然包都丢了,但双方的门都开了!)
第 2 轮:
A 发包给 B → 到 B 的 NAT → B 已开门 ✓ → 通过!
B 发包给 A → 到 A 的 NAT → A 已开门 ✓ → 通过!
连接建立!
TCP 打洞:
第 1 轮:
A 发 SYN → A 开门 ✓ → 包到 B 的 NAT → 丢弃
B 发 SYN → B 开门 ✓ → 包到 A 的 NAT → 丢弃
(双方的门也都开了!)
但问题来了...
A:「我发了 SYN,现在等 SYN-ACK...」 ← 进入等待状态
B:「我发了 SYN,现在等 SYN-ACK...」 ← 进入等待状态
双方都在等,没人再发 → 最终超时失败
核心区别:UDP 可以不停地发,TCP 发完就等着。
所以实践中的选择是:用 UDP 打洞,需要可靠传输就在 UDP 之上自己实现。这就是为什么 WireGuard、QUIC(HTTP/3)、WebRTC 都选择基于 UDP。
STUN:知道自己是谁
第一步,我们需要知道自己的「公网身份」。
STUN(Session Traversal Utilities for NAT) 协议就干这个事:
你的电脑 STUN 服务器(公网)
│ │
├── 发 UDP 包 ─────────────────►│
│ │
│◄── 告诉你「你是 1.2.3.4:5678」──┤
STUN 服务器收到你的包后,能看到你的公网 IP 和端口(经过 NAT 转换后的),然后告诉你。
STUN 是 IETF 标准协议(RFC 5389),公网上有很多免费的 STUN 服务器:
stun.l.google.com:19302stun.cloudflare.com:3478
你可以用命令行测试自己的公网地址:
# macOS
brew install stuntman
stunclient stun.l.google.com 19302
# 输出类似:Mapped address: 123.45.67.89:54321
# 这就是你经过 NAT 后的公网身份
现在你知道自己的公网身份了。
打洞的核心步骤
假设 A 和 B 都在 NAT 后面,想建立直连:
步骤 1:双方都连接协调服务器
A ──► 协调服务器 ◄── B
│
│ 交换公网地址
▼
A 知道 B 是 2.2.2.2:2000
B 知道 A 是 1.1.1.1:1000
步骤 2:同时互发 UDP 包
A 发给 2.2.2.2:2000 B 发给 1.1.1.1:1000
│ │
▼ ▼
被 B 的 NAT 丢弃 被 A 的 NAT 丢弃
(但 A 的 NAT 建立了映射) (但 B 的 NAT 建立了映射)
步骤 3:再次互发
A 发给 2.2.2.2:2000 B 发给 1.1.1.1:1000
│ │
▼ ▼
命中 B 刚建立的映射! 命中 A 刚建立的映射!
通了! 通了!
为什么要「同时」
关键在于时机。
当 A 发包给 B 时,虽然会被 B 的 NAT 丢弃,但 A 的 NAT 会建立一条映射:「我刚发了包给 2.2.2.2:2000,如果有回复就放行」。
紧接着 B 的包过来,正好命中这条映射,NAT 以为是「正常的回复」,就放行了。
这就是「打洞」—— 在 NAT 的墙上凿开一个洞。
心跳保活:防止映射过期
洞打通了,但 NAT 映射会过期。怎么办?
持续发心跳包。
每隔 25 秒:
A ──► 小数据包 ──► B
B ──► 小数据包 ──► A
就几十个字节,告诉 NAT「这个连接还活着」。NAT 收到流量就会重置过期时间。
类比:图书馆的座位,离开超过 5 分钟就被收走。怎么保住?时不时回来坐一下。
NAT 的四种类型
不同的 NAT 实现,打洞难度天差地别:
| 类型 | 特点 | 打洞难度 |
|---|---|---|
| Full Cone(全锥形) | 一旦建立映射,任何外部地址都能通过该映射访问内网设备 | 最容易 |
| Restricted Cone(受限锥形) | 只有内网设备主动联系过的 IP 才能回访 | 较容易 |
| Port Restricted Cone(端口受限锥形) | 只有内网设备主动联系过的 IP:Port 才能回访 | 中等 |
| Symmetric(对称型) | 每次连接不同目标,NAT 都分配不同的端口 | 几乎不可能 |
Full Cone:
内网发包给 A → 映射 外网:1000
之后 B、C、D 都能通过 外网:1000 访问内网 ✓
Symmetric:
内网发包给 A → 映射 外网:1000
内网发包给 B → 映射 外网:1001 ← 端口变了!
内网发包给 C → 映射 外网:1002 ← 又变了!
打洞时你通过 STUN 拿到的是 :1000,但对方连过来时 NAT 会分配 :1003,对不上。
家用路由器大多是 Port Restricted Cone,可以打洞。企业级防火墙和运营商级 NAT(CGNAT)常见 Symmetric,基本打不通。
打洞失败怎么办:TURN 中继
有些情况打洞会失败:
- 对称 NAT(Symmetric NAT):每次连接不同目标,NAT 分配不同端口,打洞基本不可能
- 严格的企业防火墙:直接封杀 UDP
这时候只能靠 TURN(Traversal Using Relays around NAT) 中继服务器:
A ──► TURN 服务器 ──► B
数据经过第三方转发,延迟高一点,但至少能通。TURN 服务器需要有公网 IP,带宽成本较高。
ICE:自动选择最佳路径
实际应用中,我们不会手动决定用直连还是中继。ICE(Interactive Connectivity Establishment) 框架会自动搞定:
ICE 收集候选地址:
1. 本地地址(192.168.1.100)
2. STUN 反射地址(经过 NAT 后的公网地址)
3. TURN 中继地址(保底方案)
│
▼
双方交换所有候选地址
│
▼
ICE 逐一尝试连接,选择能通且延迟最低的
│
▼
优先直连,实在不行才用中继
ICE 是标准协议
ICE 定义在 RFC 8445 中,是 IETF 的正式标准。它不是软件,而是一套规范化的流程:
类比:
HTTP 是协议(RFC 7230) → Nginx、curl 是实现
ICE 是协议(RFC 8445) → libnice、pion/ice 是实现
常见的 ICE 实现库:
| 库 | 语言 | 说明 |
|---|---|---|
| libnice | C | GNOME 项目,Linux 上常用 |
| pion/ice | Go | WebRTC 生态常用 |
| libwebrtc | C++ | Chrome 内置的 WebRTC 实现 |
| aiortc | Python | 异步 WebRTC/ICE 实现 |
ICE 和 STUN/TURN 的关系
从协议定义上,ICE 和 STUN/TURN 是强绑定的:
- ICE 用 STUN 来发现 Server Reflexive 候选地址
- ICE 用 STUN Binding Request 来做连通性检查
- ICE 用 TURN 作为中继候选地址的来源
但 ICE 的核心思想——多路径候选收集 + 交换 + 测试 + 择优——是通用的。Tailscale 就借鉴了这个思路:收集直连地址、NAT 映射地址、DERP 中继地址,然后智能选择最优路径。
WebRTC 视频通话、Tailscale 等都用 ICE 框架(或其思想)来实现 NAT 穿透。
STUN/TURN/ICE 三件套总结:
| 协议 | 作用 |
|---|---|
| STUN | 发现自己的公网地址 |
| TURN | 打洞失败时提供中继 |
| ICE | 协调上述两者,自动选择最佳路径 |
四、WireGuard:打通之后怎么安全通信
UDP 打洞解决了「怎么连上」的问题。但连上之后,数据在公网上裸奔,任何人都能窃听。
我们需要加密隧道。
传统 VPN 的问题
| 方案 | 代码行数 | 问题 |
|---|---|---|
| OpenVPN | ~100,000 | 配置复杂,性能一般 |
| IPSec | ~400,000 | 协议臃肿,难以审计 |
代码越多,潜在漏洞越多,越难保证安全。
WireGuard:极简主义的胜利
WireGuard 只有 约 4000 行代码。
它的设计哲学:
- 固定加密算法 —— 不给你选择,就用最好的
- 极简协议 —— 能省的都省了
- 内核级实现 —— 性能拉满
WireGuard 的加密栈
ChaCha20 → 对称加密(加密数据)
Poly1305 → 消息认证(防篡改)
Curve25519 → 密钥交换(协商密钥)
BLAKE2s → 哈希函数
这些都是现代密码学公认的最佳选择,没有历史包袱。
为什么代码少 = 更安全
代码少
│
▼
容易审计
│
▼
漏洞少
│
▼
更安全
WireGuard 的代码少到可以被一个人完整审计。2020 年,它被合并进 Linux 内核主线(5.6+),这是官方对其质量的最高认可。
WireGuard 的连接过程
1. 双方交换公钥(提前配置好)
2. 发起方发送握手包(用对方公钥加密)
3. 响应方验证并回复
4. 双方协商出会话密钥
5. 开始加密通信
整个握手只需要 1-RTT(一个往返),毫秒级建立连接。
Cryptokey Routing:公钥即身份
WireGuard 最优雅的设计之一是 Cryptokey Routing(密钥路由):用公钥来标识对端,用公钥来决定路由。
配置文件中:
[Peer]
PublicKey = abc123... ← 对端的公钥
AllowedIPs = 10.0.0.2/32 ← 这个公钥「拥有」哪些 IP
发包时:
目标 IP 10.0.0.2 → 查表 → 找到公钥 abc123... → 用该公钥加密 → 发送
收包时:
解密成功 → 检查源 IP 是否在 AllowedIPs 内 → 是则接受,否则丢弃
这个设计带来几个好处:
- 身份验证内置于路由 —— 不需要额外的认证层
- 配置极简 —— 公钥 + AllowedIPs 就完事
- 无状态 —— 没有「连接」概念,对端上线就能通信,下线也不影响
传统 VPN 需要证书、用户名密码、会话管理……WireGuard 只需要一对公钥。
WireGuard 做了什么,没做什么
| WireGuard 做的 | WireGuard 没做的 |
|---|---|
| 加密隧道 | NAT 穿透 |
| 密钥交换 | 设备发现 |
| 数据包认证 | 自动配置 |
| 心跳保活 | 多设备管理 |
WireGuard 是一个 协议和实现,不是完整的解决方案。你还需要自己搞定 NAT 穿透、密钥分发、设备管理。
五、自己搭一个 WireGuard
如果你想完全自主,可以自己部署 WireGuard。
架构
你的 VPS(有公网 IP)
运行 WireGuard
│
├──── 加密隧道 ────┐
│ │
▼ ▼
家里电脑 手机
服务端配置(VPS)
# 安装
apt install wireguard
# 生成密钥
wg genkey | tee server_private | wg pubkey > server_public
# 配置 /etc/wireguard/wg0.conf
[Interface]
PrivateKey = <服务器私钥>
Address = 10.0.0.1/24
ListenPort = 51820
[Peer] # 你的电脑
PublicKey = <电脑公钥>
AllowedIPs = 10.0.0.2/32
[Peer] # 你的手机
PublicKey = <手机公钥>
AllowedIPs = 10.0.0.3/32
# 启动
wg-quick up wg0
# 开机自启
systemctl enable wg-quick@wg0
别忘了开防火墙:
# Ubuntu/Debian (ufw)
ufw allow 51820/udp
# CentOS/RHEL (firewalld)
firewall-cmd --permanent --add-port=51820/udp
firewall-cmd --reload
# 云服务器还需要在控制台安全组放行 51820/UDP
客户端配置
[Interface]
PrivateKey = <客户端私钥>
Address = 10.0.0.2/24
[Peer]
PublicKey = <服务器公钥>
Endpoint = <服务器公网IP>:51820
AllowedIPs = 10.0.0.0/24
PersistentKeepalive = 25 # 心跳保活
完成后
- 电脑是
10.0.0.2 - 手机是
10.0.0.3 - 服务器是
10.0.0.1 - 三者在同一个虚拟局域网,互相直接访问
自建的代价
你需要:
- 一台有公网 IP 的 VPS(要花钱)
- 手动生成和交换密钥
- 手动配置每台设备
- 自己维护服务器
六、Tailscale:站在巨人肩上
如果你觉得自建太麻烦,Tailscale 是一个开箱即用的方案。
Tailscale 的本质
WireGuard(加密隧道)
+
协调服务(交换地址和密钥)
+
自动 NAT 穿透
+
设备管理后台
═══════════════
Tailscale
架构图
┌─────────────────────────────────────────────────────────┐
│ Tailscale 协调服务器 │
│ │
│ 1. 你家电脑上线 → 登记「电脑在 IP:端口A」 │
│ 2. 你手机上线 → 登记「手机在 IP:端口B」 │
│ 3. 交换公钥和地址 │
│ │
│ 然后就不管了,数据不经过这里 │
└─────────────────────────────────────────────────────────┘
│
▼
两边直接 P2P 连接
数据不经过 Tailscale
关键点:Tailscale 是媒婆,不是传话筒。 介绍完双方就撤,你们自己聊。
三种连接方式
| 方式 | 条件 | 延迟 | Tailscale 角色 |
|---|---|---|---|
| 直连 | 打洞成功 | 最低 | 只协调,不过数据 |
| 中继 | 打洞失败 | 较高 | 转发加密数据 |
| 子网路由 | 特殊配置 | 取决于路径 | 只协调 |
大多数情况(90%+)能打洞成功,直连。
怎么知道是不是直连
tailscale status
输出:
100.64.0.1 my-pc active; direct 1.2.3.4:41641 ← 直连
100.64.0.2 my-phone active; relay "tok" ← 中继
Tailscale 资费与相关网站
官方网站:
- 官网:https://tailscale.com
- 下载:https://tailscale.com/download
- 定价:https://tailscale.com/pricing
- 文档:https://tailscale.com/kb
资费方案:
| 计划 | 价格 | 设备数 | 用户数 | 适用场景 |
|---|---|---|---|---|
| Personal(个人) | 免费 | 100 台 | 3 人 | 个人/家庭使用 |
| Starter | $6/用户/月 | 无限 | 无限 | 小团队 |
| Premium | 联系销售 | 无限 | 无限 | 企业(审计、SSO、SCIM) |
免费版功能:
| 项目 | 额度 |
|---|---|
| 设备数 | 100 台 |
| 用户数 | 3 人 |
| 子网路由 | 支持 |
| SSH 直连 | 支持 |
| Funnel(公网暴露) | 支持 |
| 流量 | 不限 |
个人使用免费版完全够。收费版主要加企业功能(审计日志、ACL 策略、SSO 单点登录)。
不想用 Tailscale 服务? 可以用 Headscale(开源协调服务器)+ 官方 Tailscale 客户端,完全自托管。
5 分钟体验
# 1. 下载安装
# https://tailscale.com/download
# 2. 登录(电脑和手机都登录同一个账号)
tailscale up
# 3. 查看分配的 IP
tailscale ip
# 比如 100.64.0.1
# 4. 从手机 SSH 到电脑
ssh user@100.64.0.1
搞定。不需要公网 IP、端口映射、DDNS。
七、Tailscale Funnel:暴露到公网
如果你想让不装 Tailscale 的人也能访问你的服务,可以用 Tailscale Funnel:
# 本地跑一个服务,比如端口 8080
python -m http.server 8080
# 用 Funnel 暴露
tailscale funnel 8080
Tailscale 会给你一个公网地址:
https://my-pc.tail12345.ts.net/
任何人都能访问。
原理:
公网用户 ──► Tailscale 边缘服务器 ──► WireGuard 隧道 ──► 你的电脑
这时候流量确实经过 Tailscale,但仍然是端到端加密的。
Funnel 安全注意事项
Funnel 把你的本地服务暴露到公网,意味着 全世界都能访问。使用前请确保:
- 不要暴露敏感服务 —— 数据库、管理后台、未认证的 API 都不应该用 Funnel
- 加上认证 —— 如果服务本身没有登录机制,考虑加一层 Basic Auth 或 OAuth
- 注意日志和监控 —— 公网暴露后可能会收到扫描和攻击流量
- 临时使用后关闭 ——
tailscale funnel off关掉 Funnel
Funnel 适合临时演示、Webhook 回调等场景,不建议用于长期暴露生产服务。
八、完全自主:Headscale
如果你不想依赖 Tailscale 的协调服务器,可以用开源替代品 Headscale:
你的 VPS 跑 Headscale(协调服务器)
│
▼
手机/电脑 用 Tailscale 客户端连接
这样你有 Tailscale 客户端的便利,协调服务也是自己的,完全自主。
九、总结:一层套一层
问题:NAT 让内网设备无法被外部访问
│
▼
原理:UDP 打洞(同时敲门,骗过 NAT)
│
▼
协议:WireGuard(加密隧道,不含打洞)
│
▼
产品:Tailscale(WireGuard + 协调 + 管理)
│
▼
自主:Headscale(开源协调服务器)
每一层解决不同的问题:
| 层次 | 解决的问题 |
|---|---|
| UDP 打洞 | 穿透 NAT |
| WireGuard | 安全通信 |
| Tailscale | 开箱即用 |
| Headscale | 完全自主 |
技术选型指南
| 你的需求 | 推荐方案 |
|---|---|
| 快速解决问题,不想折腾 | Tailscale |
| 想学习原理,愿意折腾 | 裸 WireGuard |
| 要完全自主又不想太折腾 | Headscale + Tailscale 客户端 |
| 只是临时用一下 | SSH 隧道 |
附录:常见问题
Q: 为什么我打洞总是失败?
可能原因:
- 对称 NAT(Symmetric NAT)—— 换个网络试试
- 防火墙封了 UDP —— 检查防火墙设置
- 运营商级 NAT(CGNAT)—— 多层 NAT,打洞更难
Q: Tailscale 安全吗?会不会看我的数据?
Tailscale 协调服务器只交换地址和公钥,数据走 WireGuard 端到端加密,Tailscale 看不到内容。即使走 DERP 中继,数据也是加密的。
Q: WireGuard 比 OpenVPN 快多少?
通常快 3-4 倍,延迟低 30-50%。因为 WireGuard 在内核态运行,加密算法也更高效。
Q: 心跳包会耗多少流量?
约 100KB/天,几乎可以忽略。
「从问题出发,层层深入」—— 这是理解技术的最佳路径。