欢迎光临
我们一直在努力

WebRTC(十五):NAT穿透机制深度解析

NAT与WebRTC的问题背景

在互联网环境中,大多数终端设备并不直接拥有公网 IP,而是位于 NAT(Network Address Translation,网络地址转换) 设备之后。例如家庭路由器、企业网关等都会对内部网络进行 NAT 转换。

典型网络结构如下:

公网
|
+———–+
| Router | (NAT)
+———–+
/ \\
192.168.1.2 192.168.1.3
ClientA ClientB

当两个客户端尝试建立 P2P 连接时会遇到问题:

  • 内网地址无法直接访问
  • NAT 会改变端口
  • 防火墙阻止外部连接

因此 WebRTC 需要一套机制实现 NAT 穿透。

WebRTC 采用的解决方案是:

ICE + STUN + TURN

三者协同工作完成 NAT 穿透。

WebRTC NAT穿透整体架构

WebRTC 的 NAT 穿透架构如下

Signaling Server
|
+———-+———-+
| |
Client A Client B
| |
|—-STUN Request—–|
| |
|—–ICE Candidate Exchange—-|
| |
|—-Connectivity Check(STUN)—|
| |
|———P2P Media———–|

整个过程分为五个阶段:

  • 信令交换
  • Candidate 收集
  • Candidate 交换
  • ICE 连通性检测
  • 建立媒体连接
  • NAT类型

    不同NAT类型对 P2P 连接影响很大。

    常见NAT类型包括:

    NAT 类型特点P2P 成功率
    Full Cone NAT 任意外部主机可访问映射端口 高
    Restricted NAT 只允许访问过的 IP 中
    Port Restricted NAT IP+端口限制 中
    Symmetric NAT 每个连接不同映射 低

    NAT 示例:

    ClientA 内网IP: 192.168.1.2:5000

    NAT映射:

    公网IP: 8.134.10.20:40000

    远端访问:

    8.134.10.20:40000

    即可访问 ClientA。

    但 对称 NAT 会为不同目标生成不同端口:

    ClientA → STUN Server
    映射: 8.134.10.20:40000

    ClientA → ClientB
    映射: 8.134.10.20:42000

    这种情况 P2P 连接往往失败。

    STUN协议原理

    STUN(Session Traversal Utilities for NAT)用于获取公网地址。

    STUN工作流程:

    Client —-STUN Request—-> STUN Server
    Client <—STUN Response—- STUN Server

    响应中包含:

    Mapped Address = 公网IP + 端口

    流程图:

    Client
    |
    | STUN Binding Request
    v
    +————+
    | STUN Server|
    +————+
    |
    | 返回公网IP
    v
    Client

    示例:

    本地地址: 192.168.1.2:5000

    STUN返回:

    8.134.10.20:40000

    此地址称为:

    Server Reflexive Candidate

    STUN Server 的部署形态

    STUN Server 在实际部署中通常有三种形态。

    1. 独立 STUN 服务

    最常见形态是 独立服务程序。

    例如:

    coturn
    reTurn
    stund

    部署方式:

    Linux Server
    |
    |– STUN Server
    |
    公网IP

    客户端配置:

    stun:stun.example.com:3478

    特点:

    • 实现简单
    • 资源消耗低
    • 支持高并发

    2. STUN + TURN 一体服务

    在 WebRTC 系统中,STUN 通常与 TURN 一起部署。

    最典型实现:

    coturn

    架构:

    Internet
    |
    +—————-+
    | coturn |
    | STUN + TURN |
    +—————-+

    优点:

    • 一套服务
    • 支持 NAT 穿透 + 中继
    • WebRTC 标准部署方式

    WebRTC 推荐配置:

    stun:turn.example.com:3478
    turn:turn.example.com:3478

    3. 云 STUN 服务

    很多系统直接使用 公共 STUN Server。

    例如:

    Google STUN

    地址:

    stun.l.google.com:19302

    架构:

    Client
    |
    v
    Google STUN Server

    优点:

    • 不需要自己部署
    • 稳定

    缺点:

    • 不可控
    • 有访问限制

    STUN Server获取公网地址

    NAT地址转换原理

    假设客户端在内网:

    Client
    192.168.1.5:5000

    通过路由器 NAT 访问公网:

    Router NAT

    NAT 会创建一个映射:

    192.168.1.5:5000
    ↓
    8.134.20.10:42000

    此时公网看到的地址是:

    8.134.20.10:42000

    通信流程:

    Client → NAT → Internet → STUN Server

    数据包在 NAT 设备处被修改。

    STUN获取公网地址的核心机制

    STUN Server 接收客户端请求时:

    recvfrom()

    操作系统会提供 发送方地址:

    source IP
    source port

    这正是 NAT 映射后的公网地址。

    示例:

    Client 内网

    192.168.1.5:5000

    经过 NAT:

    8.134.20.10:42000

    STUN Server 接收到的数据:

    src_ip = 8.134.20.10
    src_port = 42000

    STUN Server 就把这个地址写入响应。

    STUN工作完整流程

    完整流程如下:

    Client STUN Server
    | |
    | Binding Request |
    |————————>|
    | |
    | |
    | 读取 source IP/port |
    | |
    | Binding Response |
    |<————————|

    客户端最终得到:

    公网IP + 公网端口

    STUN协议返回字段

    STUN Server 在响应中返回:

    XOR-MAPPED-ADDRESS

    例如:

    XOR-MAPPED-ADDRESS
    IP = 8.134.20.10
    PORT = 42000

    客户端解析后得到自己的公网地址。

    STUN Server实现逻辑

    STUN Server 的核心逻辑非常简单:

    UDP Socket
    |
    recvfrom()
    |
    获取source ip/port
    |
    构造STUN Response
    |
    sendto()

    流程图:

    +—————-+
    Client —>| STUN Server |
    +—————-+
    |
    | recvfrom()
    |
    获取 source IP
    获取 source port
    |
    构造 STUN Response
    |
    v
    Client <— sendto()

    STUN Server返回公网地址的原因

    核心原因是NAT修改了 IP 包头。

    数据包变化:

    客户端发送:

    SRC IP: 192.168.1.5
    SRC PORT: 5000
    DST IP: STUN_SERVER

    经过 NAT:

    SRC IP: 8.134.20.10
    SRC PORT: 42000
    DST IP: STUN_SERVER

    STUN Server 看到的就是:

    8.134.20.10:42000

    因此无需计算。

    完整 STUN Server (C++)

    #include <iostream>
    #include <vector>
    #include <cstring>
    #include <arpa/inet.h>
    #include <sys/socket.h>
    #include <unistd.h>
    #include <netinet/in.h>

    using namespace std;

    static const uint16_t STUN_BINDING_REQUEST = 0x0001;
    static const uint16_t STUN_BINDING_RESPONSE = 0x0101;

    static const uint16_t STUN_ATTR_XOR_MAPPED_ADDRESS = 0x0020;

    static const uint32_t STUN_MAGIC_COOKIE = 0x2112A442;

    #pragma pack(push,1)

    struct StunHeader
    {
    uint16_t type;
    uint16_t length;
    uint32_t magic;
    uint8_t transaction_id[12];
    };

    struct StunAttributeHeader
    {
    uint16_t type;
    uint16_t length;
    };

    #pragma pack(pop)

    class Buffer
    {
    public:

    vector<uint8_t> data;

    void append(const void* buf,size_t len)
    {
    const uint8_t* p=(const uint8_t*)buf;
    data.insert(data.end(),p,p+len);
    }

    uint8_t* ptr()
    {
    return data.data();
    }

    size_t size() const
    {
    return data.size();
    }
    };

    class StunMessage
    {
    public:

    StunHeader header;

    vector<uint8_t> attributes;

    StunMessage()
    {
    memset(&header,0,sizeof(header));
    }

    };

    class StunParser
    {
    public:

    static bool parse(const uint8_t* buf,int len,StunMessage& msg)
    {

    if(len < sizeof(StunHeader))
    return false;

    memcpy(&msg.header,buf,sizeof(StunHeader));

    msg.header.type = ntohs(msg.header.type);
    msg.header.length = ntohs(msg.header.length);
    msg.header.magic = ntohl(msg.header.magic);

    if(msg.header.magic != STUN_MAGIC_COOKIE)
    return false;

    int attr_len = msg.header.length;

    if(attr_len + sizeof(StunHeader) > len)
    return false;

    msg.attributes.assign(buf + sizeof(StunHeader),
    buf + sizeof(StunHeader) + attr_len);

    return true;
    }
    };

    class StunBuilder
    {
    public:

    static void addXorMappedAddress(Buffer& buffer,
    const sockaddr_in& addr,
    const uint8_t transaction_id[12])
    {

    StunAttributeHeader attr;

    attr.type = htons(STUN_ATTR_XOR_MAPPED_ADDRESS);
    attr.length = htons(8);

    buffer.append(&attr,sizeof(attr));

    uint8_t family = 0x01;

    uint16_t port = ntohs(addr.sin_port);
    uint32_t ip = ntohl(addr.sin_addr.s_addr);

    port ^= (STUN_MAGIC_COOKIE >> 16);
    ip ^= STUN_MAGIC_COOKIE;

    uint16_t xport = htons(port);
    uint32_t xip = htonl(ip);

    uint8_t reserved = 0;

    buffer.append(&reserved,1);
    buffer.append(&family,1);
    buffer.append(&xport,2);
    buffer.append(&xip,4);

    }

    static Buffer buildBindingResponse(const StunMessage& req,
    const sockaddr_in& client_addr)
    {

    Buffer buf;

    StunHeader header;

    header.type = htons(STUN_BINDING_RESPONSE);
    header.magic = htonl(STUN_MAGIC_COOKIE);

    memcpy(header.transaction_id,
    req.header.transaction_id,
    12);

    header.length = 0;

    buf.append(&header,sizeof(header));

    addXorMappedAddress(buf,client_addr,req.header.transaction_id);

    uint16_t attr_len = buf.size() – sizeof(StunHeader);

    uint16_t net_len = htons(attr_len);

    memcpy(buf.ptr()+2,&net_len,2);

    return buf;
    }
    };

    class UdpServer
    {
    public:

    int sock;

    UdpServer()
    {
    sock = -1;
    }

    bool start(int port)
    {

    sock = socket(AF_INET,SOCK_DGRAM,0);

    if(sock < 0)
    {
    cerr<<"socket failed\\n";
    return false;
    }

    sockaddr_in addr;

    memset(&addr,0,sizeof(addr));

    addr.sin_family = AF_INET;
    addr.sin_addr.s_addr = INADDR_ANY;
    addr.sin_port = htons(port);

    if(bind(sock,(sockaddr*)&addr,sizeof(addr))<0)
    {
    cerr<<"bind failed\\n";
    return false;
    }

    cout<<"STUN server listening "<<port<<endl;

    return true;
    }

    void run()
    {

    while(true)
    {

    uint8_t buf[1500];

    sockaddr_in client;

    socklen_t len = sizeof(client);

    int n = recvfrom(sock,
    buf,
    sizeof(buf),
    0,
    (sockaddr*)&client,
    &len);

    if(n <= 0)
    continue;

    handlePacket(buf,n,client);

    }

    }

    void handlePacket(uint8_t* buf,
    int len,
    sockaddr_in& client)
    {

    StunMessage msg;

    if(!StunParser::parse(buf,len,msg))
    {
    return;
    }

    if(msg.header.type != STUN_BINDING_REQUEST)
    {
    return;
    }

    char ip[64];

    inet_ntop(AF_INET,&client.sin_addr,ip,sizeof(ip));

    int port = ntohs(client.sin_port);

    cout<<"Binding Request from "
    <<ip<<":"<<port<<endl;

    Buffer resp = StunBuilder::buildBindingResponse(msg,client);

    sendto(sock,
    resp.ptr(),
    resp.size(),
    0,
    (sockaddr*)&client,
    sizeof(client));

    }

    };

    int main(int argc,char* argv[])
    {

    int port = 3478;

    if(argc > 1)
    port = atoi(argv[1]);

    UdpServer server;

    if(!server.start(port))
    return -1;

    server.run();

    return 0;
    }

    ICE协议

    ICE(Interactive Connectivity Establishment)是 WebRTC NAT 穿透的核心机制。

    ICE 负责:

    • 收集候选地址
    • 交换 Candidate
    • 连通性检测
    • 选择最佳路径

    ICE Candidate 类型:

    Host Candidate
    Server Reflexive Candidate
    Relay Candidate

    Candidate 收集过程

    ICE Agent 会收集三类 Candidate。

    1. Host Candidate

    本机地址:

    192.168.1.2:5000

    2. STUN Candidate(srflx)

    通过 STUN 获取:

    8.134.10.20:40000

    3. TURN Candidate(relay)

    通过 TURN 获取中继地址:

    34.210.10.5:52000

    Candidate 收集流程:

    Client
    |
    | 获取本地地址
    v
    Host Candidate

    |
    | STUN
    v
    srflx Candidate

    |
    | TURN
    v
    Relay Candidate

    Candidate 交换

    Candidate 通过信令服务器交换。

    信令服务器可以是:

    • WebSocket
    • HTTP
    • SIP
    • MQTT

    流程:

    ClientA → Signaling → ClientB
    ClientB → Signaling → ClientA

    交换内容:

    SDP + ICE Candidate

    示例:

    a=candidate:1 1 udp 2130706431 192.168.1.2 5000 typ host
    a=candidate:2 1 udp 1694498815 8.134.10.20 40000 typ srflx
    a=candidate:3 1 udp 33554431 34.210.10.5 52000 typ relay

    ICE连通性检测

    双方得到 Candidate 后会形成 Candidate Pair。

    例如:

    A: 192.168.1.2:5000
    B: 10.0.0.5:6000

    组合为:

    Pair1
    Pair2
    Pair3

    检测流程:

    ClientA —-STUN Binding—-> ClientB
    ClientB <—-STUN Response—- ClientA

    流程图:

    ClientA ClientB
    | |
    |—-STUN Binding Request–>|
    | |
    |<—STUN Binding Response–|
    | |

    成功则该路径有效。

    路径选择机制

    ICE 会根据优先级选择最佳路径。

    优先级顺序:

    Host > srflx > relay

    原因:

    类型延迟成本
    Host 最低 0
    srflx 低 低
    relay 高 高

    ICE 会尝试:

    host-host
    host-srflx
    srflx-srflx
    relay

    最终选择成功的最高优先级路径。

    TURN中继机制

    当 NAT 穿透失败时,使用 TURN 中继。

    通信路径:

    ClientA → TURN → ClientB

    流程图:

    ClientA
    |
    | RTP
    v
    +———+
    | TURN |
    | Server |
    +———+
    |
    | RTP
    v
    ClientB

    TURN 服务器负责:

    • 分配 Relay 地址
    • 转发 RTP / RTCP
    • 维护连接状态

    缺点:

    • 延迟增加
    • 消耗服务器带宽

    优点:

    • 成功率最高

    Trickle ICE优化

    传统 ICE 需要等待 Candidate 收集完成。

    Trickle ICE 则 边收集边发送。

    流程:

    收集一个 Candidate
    |
    v
    立即发送
    |
    v
    远端开始检测

    流程图:

    ClientA ClientB
    | |
    | candidate1 —-> |
    | candidate2 —-> |
    | candidate3 —-> |

    优点:

    • 连接建立更快
    • 用户体验更好

    完整NAT穿透流程图

    完整流程如下:

    +——————-+
    | Signaling Server |
    +——————-+
    ^ ^
    | |
    | SDP/ICE |
    | |
    ClientA ClientB
    | |
    |—STUN–|
    | |
    |—TURN–|
    | |
    |–Candidate Exchange–|
    | |
    |–ICE Connectivity Check–|
    | |
    |——P2P Media——|

    如果 P2P 失败:

    ClientA → TURN → ClientB

    总结

    WebRTC NAT 穿透是通过 ICE + STUN + TURN 三个核心技术实现的。

    主要流程包括:

  • 收集 Candidate
  • 交换 Candidate
  • ICE 连通性检测
  • 选择最佳路径
  • 建立媒体连接
  • Candidate 类型:

    Host
    Server Reflexive
    Relay

    在大多数情况下 WebRTC 可以通过 STUN 实现 P2P 直连。当 NAT 类型复杂或防火墙严格时,则通过 TURN 中继完成通信。

    合理部署 STUN/TURN 服务器和优化 ICE 策略,是提升 WebRTC 连接成功率和系统性能的重要手段。

    赞(0)
    未经允许不得转载:171主机测评 » WebRTC(十五):NAT穿透机制深度解析
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址