# STUN 协议 · ICE 的万能工具

> STUN 是 ICE 里出场最多的协议，身兼两职：问出自己的公网地址（srflx 候选），以及给候选对做连通性检查（STUN ping）。这一章读 p2p 的 stun.cc，讲清 STUN 消息结构、Binding 请求/响应、XOR-MAPPED-ADDRESS 怎么让你「照镜子」看到公网地址。

- 作者：David（道雾轩）
- 专栏：音视频与 WebRTC 详解（https://daiw.org/manual/webrtc.md）
- 最后更新：2026-07-14
- 原文：https://daiw.org/manual/webrtc/stun-protocol
- 转载与引用：请注明出处并附原文链接（https://daiw.org/about/copyright）

# STUN 协议 · ICE 的万能工具

在 [ICE](https://daiw.org/manual/webrtc/ice-overview) 里，有一个协议出场最频繁、身兼数职——**STUN**。[采集 srflx 候选](https://daiw.org/manual/webrtc/port-allocator)靠它，[连通性检查](https://daiw.org/manual/webrtc/ice-overview)也靠它，连保活都靠它。搞懂 STUN，ICE 就通了一大半。这一章读 `p2p` 里的 STUN 实现。

## STUN 是什么：一面「照公网的镜子」

**STUN（Session Traversal Utilities for NAT）** 最基础的用途，是回答一个问题：**「我的公网地址是什么？」** 机制简单得漂亮：

<Callout type="info">
  **你向 STUN 服务器发一个包，服务器如实告诉你「我看到这个包来自哪个地址」——那就是你在 NAT 外面的样子。** 你的包出 [NAT](https://daiw.org/manual/webrtc/ice-overview) 时，源地址被 NAT 改成了公网地址 + 某个端口；STUN 服务器收到后，把这个「它看到的源地址」原样回给你。于是你就**照到了自己的公网镜像**——这正是 [srflx 候选](https://daiw.org/manual/webrtc/candidates)。

  这就是 [`StunPort`](https://daiw.org/manual/webrtc/port-allocator) 采集 srflx 候选的原理：向配置的 STUN 服务器（`RTCConfiguration` 里的 `iceServers`）发一个 STUN 请求，拿回响应里的公网地址，当作一个候选发给对端。STUN 服务器**极轻量**（只是「照镜子」，不转发媒体），所以公共 STUN 服务器遍地都是、几乎免费——这和要转发所有媒体、成本高昂的 [TURN](https://daiw.org/manual/webrtc/source-map) 形成鲜明对比。
</Callout>

## STUN 消息：Binding 请求与响应

STUN 消息的核心类型，`p2p` 的 `stun.h` 里定义得清清楚楚：

```cpp
// p2p 的 stun.h(节选)
STUN_BINDING_REQUEST    = 0x0001,   // Binding 请求(「告诉我你看到的我」)
STUN_BINDING_INDICATION = 0x0011,   // Binding 指示(单向,常用于保活)
STUN_BINDING_RESPONSE   = 0x0101,   // Binding 响应(「我看到你来自 X」)
```

一条 STUN 消息的结构：**消息类型**（上面这些）+ **事务 ID（transaction id）**（把响应和请求对上号）+ 一串**属性（attributes）**。最关键的属性是响应里的 **`XOR-MAPPED-ADDRESS`**——它装着「服务器看到的你的地址」（做了 XOR 混淆，避开某些 NAT 会篡改裸地址的毛病）。`stun.cc` 里 `StunMessage` 类负责这些消息的**解析与构造**（读写字节、校验、加属性）。

## STUN 的第二职：连通性检查

STUN 在 ICE 里更繁忙的角色，是**连通性检查**——即[给候选对做「ping」](https://daiw.org/manual/webrtc/ice-overview)。[`Connection`](https://daiw.org/manual/webrtc/source-map) 对象通过互发 **Binding 请求 / 响应**，来验证「这条候选对，包能不能一来一回地通」：

- 本端 `Connection` 向对端候选发一个 `STUN_BINDING_REQUEST`。
- 对端收到，回一个 `STUN_BINDING_RESPONSE`。
- **收到响应 = 这条路双向可达**——这个候选对「通了」。

这些检查用的 Binding 请求，和查公网地址的那种，格式一样但**带上了 [ICE 凭据](https://daiw.org/manual/webrtc/sdp-and-jsep)**：请求里的 `USERNAME` 属性带着协商好的 ice-ufrag，还有 `MESSAGE-INTEGRITY` 用 ice-pwd 做的完整性校验。这样对端能确认「这个探测确实来自我要通话的那一方」，而非攻击者。**同一个 STUN Binding，查地址时是『照镜子』，检查时是『敲门确认』。**

## 保活与保持 NAT 绑定

选定一条连接后，STUN 还有第三职：**保活**。`g3doc/ice.md` 说明，`Connection` 会持续发 STUN 探测，目的有二——**维持 NAT 绑定**（NAT 的端口映射会超时，不发包就被回收，路就断了）和**确认对端还想继续**（对方若走了，探测会失败）。所以哪怕通话中一切正常，STUN ping 也在底下默默地、周期性地跳动，像心跳一样维系着这条来之不易的通路。

```mermaid
sequenceDiagram
    participant C as 客户端(NAT 后)
    participant S as STUN 服务器
    participant P as 对端
    Note over C,S: 用途一:采集 srflx 候选(照镜子)
    C->>S: STUN Binding 请求
    S->>C: Binding 响应(XOR-MAPPED-ADDRESS = 你的公网地址)
    Note over C,P: 用途二:连通性检查(敲门,带 ICE 凭据)
    C->>P: STUN Binding 请求(USERNAME + MESSAGE-INTEGRITY)
    P->>C: Binding 响应 → 这条候选对「通了」
    Note over C,P: 用途三:选定后周期性保活
    C->>P: STUN ping(维持 NAT 绑定 + 确认对端在)
```

## 小结与下一章

**STUN 是 ICE 的万能工具，身兼三职**：① [采集 srflx](https://daiw.org/manual/webrtc/port-allocator)——向 STUN 服务器发 Binding 请求，响应里的 `XOR-MAPPED-ADDRESS` 就是你的公网镜像（服务器极轻量、近乎免费）；② [连通性检查](https://daiw.org/manual/webrtc/ice-overview)——[`Connection`](https://daiw.org/manual/webrtc/source-map) 互发 Binding 请求 / 响应验证候选对可达，请求带 [ICE 凭据](https://daiw.org/manual/webrtc/sdp-and-jsep)防冒充；③ 保活——周期性 ping 维持 NAT 绑定。消息类型 `BINDING_REQUEST`（0x0001）/ `RESPONSE`（0x0101），`stun.cc` 的 `StunMessage` 负责解析构造。

STUN 能穿越大多数 NAT，但**对称 NAT 会让它失效**——这时只能上 **TURN 中继**。下一章看这个「保底方案」。👉 [TURN 中继](https://daiw.org/manual/webrtc/turn)。

## 参考资料

- [1] libwebrtc 源码.`p2p` 的 `stun.h` / `stun.cc`（`StunMessage`、Binding 类型、`XOR-MAPPED-ADDRESS`）。
- [2] libwebrtc 源码.`p2p/base/stun_port.h`（`StunPort` 采集 srflx）、`g3doc/ice.md`。
- [3] IETF RFC 8489「Session Traversal Utilities for NAT （STUN）」。
