从「在外面怎么访问家里电脑」聊起:NAT 穿透、UDP 打洞与 WireGuard

发表信息: by

从「在外面怎么访问家里电脑」聊起: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 ──┘

当你电脑访问外网时:

  1. 电脑发包:192.168.1.101:12345 → 目标服务器
  2. 路由器 NAT 转换:123.45.67.89:54321 → 目标服务器
  3. 路由器记住:54321 端口 = 电脑的 12345 端口
  4. 服务器回复到 123.45.67.89:54321
  5. 路由器查表,转发给 192.168.1.101:12345

核心问题:内网设备没有「真正的公网身份」

这就是问题所在:

你想做的 现实情况
连接 192.168.1.101 这个地址在公网不存在
连接 123.45.67.89 路由器不知道转发给谁

NAT 映射有两个致命特点:

  1. 临时的 —— 几分钟没用就过期删除
  2. 单向的 —— 只有内网主动发起才会建立映射,外部无法主动连入

这就是为什么你在外面无法直接访问家里电脑。

三、打洞:如何骗过 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:19302
  • stun.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 行代码

它的设计哲学:

  1. 固定加密算法 —— 不给你选择,就用最好的
  2. 极简协议 —— 能省的都省了
  3. 内核级实现 —— 性能拉满

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 内 → 是则接受,否则丢弃

这个设计带来几个好处:

  1. 身份验证内置于路由 —— 不需要额外的认证层
  2. 配置极简 —— 公钥 + AllowedIPs 就完事
  3. 无状态 —— 没有「连接」概念,对端上线就能通信,下线也不影响

传统 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 资费与相关网站

官方网站:

资费方案:

计划 价格 设备数 用户数 适用场景
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: 为什么我打洞总是失败?

可能原因:

  1. 对称 NAT(Symmetric NAT)—— 换个网络试试
  2. 防火墙封了 UDP —— 检查防火墙设置
  3. 运营商级 NAT(CGNAT)—— 多层 NAT,打洞更难

Q: Tailscale 安全吗?会不会看我的数据?

Tailscale 协调服务器只交换地址和公钥,数据走 WireGuard 端到端加密,Tailscale 看不到内容。即使走 DERP 中继,数据也是加密的。

Q: WireGuard 比 OpenVPN 快多少?

通常快 3-4 倍,延迟低 30-50%。因为 WireGuard 在内核态运行,加密算法也更高效。

Q: 心跳包会耗多少流量?

约 100KB/天,几乎可以忽略。


「从问题出发,层层深入」—— 这是理解技术的最佳路径。