Skip to content

UDP Full-Cone NAT ​

TUN 处理器为 UDP 流量实现了 Full-Cone NAT,允许来自任意远程地址的返回数据包到达原始客户端——不仅限于数据包最初发送到的地址。

源码:proxy/tun/udp_fullcone.go

NAT 类型说明 ​

Symmetric NAT:       Client:1234 → Server_A:80  ✓
                     Server_B:80 → Client:1234  ✗ (不同服务器,被阻止)

Full-Cone NAT:       Client:1234 → Server_A:80  ✓
                     Server_B:80 → Client:1234  ✓ (任何服务器都可以响应)

Full-Cone NAT 适用于以下场景:

  • WebRTC / P2P 应用
  • 游戏服务器(基于 UDP)
  • STUN/TURN 协议
  • 任何响应来自不同地址的协议

架构 ​

mermaid
flowchart LR
    subgraph TUN["TUN 设备"]
        App([应用程序])
    end

    subgraph UDPHandler["UDP 连接处理器"]
        Map["连接映射表<br/>src → udpConn"]
        Conn1["udpConn A<br/>(src=10.0.0.1:5000)"]
        Conn2["udpConn B<br/>(src=10.0.0.1:6000)"]
    end

    subgraph Xray["Xray 路由"]
        Handler["TUN Handler"]
        Dispatcher["Dispatcher"]
    end

    App -->|"UDP 数据包<br/>src=10.0.0.1:5000<br/>dst=8.8.8.8:53"| UDPHandler
    UDPHandler -->|"按 src 查找"| Map
    Map -->|"新连接"| Conn1
    Conn1 -->|"HandleConnection()"| Handler
    Handler -->|"DispatchLink()"| Dispatcher

    Dispatcher -->|"响应数据包"| Conn1
    Conn1 -->|"writeRawUDPPacket()"| App

连接处理器 ​

go
type udpConnectionHandler struct {
    sync.Mutex
    udpConns map[net.Destination]*udpConn  // keyed by SOURCE address
    handleConnection func(conn net.Conn, dest net.Destination)
    writePacket      func(data []byte, src, dst net.Destination) error
}

关键设计:连接映射表仅以源地址为键,而非(源地址, 目的地址)对。这正是实现 Full-Cone 的核心——任何服务器都可以向同一源地址发送回复。

数据包入站 ​

go
func (u *udpConnectionHandler) HandlePacket(src, dst net.Destination, data []byte) bool {
    u.Lock()
    conn, found := u.udpConns[src]
    if !found {
        // New source: create connection
        egress := make(chan []byte, 16)
        conn = &udpConn{handler: u, egress: egress, src: src, dst: dst}
        u.udpConns[src] = conn

        // Dispatch to Xray routing (in goroutine)
        go u.handleConnection(conn, dst)
    }
    u.Unlock()

    // Forward packet data to the connection's egress channel
    select {
    case conn.egress <- data:  // delivered
    default:                    // channel full, discard
    }
    return true
}

虚拟 UDP 连接 ​

go
type udpConn struct {
    handler *udpConnectionHandler
    egress  chan []byte        // incoming packets (from TUN)
    src     net.Destination    // client source address
    dst     net.Destination    // original destination
}

udpConn 实现了 net.Conn 接口,供 Xray 调度器使用:

go
// Read: receive packets from the egress channel
func (c *udpConn) Read(p []byte) (int, error) {
    data, ok := <-c.egress
    if !ok { return 0, io.EOF }
    return copy(p, data), nil
}

// Write: construct raw UDP packet back to source
func (c *udpConn) Write(p []byte) (int, error) {
    // REVERSE src/dst: response goes from dst → src
    err := c.handler.writePacket(p, c.dst, c.src)
    return len(p), err
}

WriteMultiBuffer(支持逐包目的地址) ​

go
func (c *udpConn) WriteMultiBuffer(mb buf.MultiBuffer) error {
    for _, b := range mb {
        dst := c.dst
        if b.UDP != nil {
            dst = *b.UDP  // Use per-packet destination from XUDP
        }
        // Validate address family matches
        if dst.Address.Family() != c.dst.Address.Family() {
            continue
        }
        // Send reversed: dst→src
        c.handler.writePacket(b.Bytes(), dst, c.src)
    }
    return nil
}

b.UDP 字段允许 XUDP 为每个数据包指定不同的目的地址(Full-Cone:可从任意地址返回)。

原始数据包构造 ​

返回的 UDP 数据包必须构造为原始 IP 数据包(因为没有使用 gVisor 的 UDP 转发器):

IPv4 数据包:
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| Ver|IHL|  TOS  |         Total Length           |    TTL=64  |
| Protocol=17(UDP)|  Header Checksum  |   Source IP (4 bytes)  |
|  Destination IP (4 bytes)  |                                  |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
UDP 头部:
+--+--+--+--+--+--+--+--+
| Src Port  | Dst Port  |
|  Length    | Checksum  |
+--+--+--+--+--+--+--+--+
|       Payload          |
+--+--+--+--+--+--+--+--+

数据包通过 WriteRawPacket() 注入回 gVisor 协议栈,后续流程:

  1. 经 gVisor 网络层处理
  2. 传递到 TUN Endpoint
  3. TUN 设备发送到内核
  4. 内核将数据包递交给应用程序

连接生命周期 ​

mermaid
sequenceDiagram
    participant App as 应用程序
    participant TUN
    participant UH as UDP 处理器
    participant UC as udpConn
    participant Xray as Xray Dispatcher

    App->>TUN: UDP 数据包 (src=A, dst=B)
    TUN->>UH: HandlePacket(A, B, data)
    UH->>UH: udpConns[A] 未找到
    UH->>UC: 创建 udpConn(src=A, dst=B)
    UH-->>Xray: go HandleConnection(conn, B)
    UH->>UC: egress <- data

    Xray->>UC: Read() [阻塞于 egress channel]
    UC-->>Xray: data
    Xray->>Xray: 路由 + 转发到出站

    Note over Xray: 响应到达(可能来自 C 而非 B)

    Xray->>UC: Write(response) 或 WriteMultiBuffer
    UC->>UH: writeRawUDPPacket(response, B→A)
    UH->>TUN: 原始 IP+UDP 数据包
    TUN->>App: UDP 响应

    App->>TUN: 另一个数据包 (src=A, dst=D)
    TUN->>UH: HandlePacket(A, D, data)
    UH->>UH: udpConns[A] 已存在!
    UH->>UC: egress <- data (复用连接)

    Note over UC: Xray 完成后连接关闭
    UC->>UH: connectionFinished(A)
    UH->>UH: delete udpConns[A]

实现要点 ​

  1. 以源地址为键的映射表:连接映射表仅按源地址索引(map[net.Destination]*udpConn)。这就是 Full-Cone 模式:每个客户端源地址一个连接,与目的地址无关。

  2. Channel 缓冲:egress channel 容量为 16。如果已满,数据包将被丢弃。这可以防止内存问题,但在突发负载下可能丢失 UDP 数据包。

  3. 无超时清理机制:没有显式的基于超时的空闲 UDP 连接清理。连接在 Xray 调度器完成时被清理(由调度器的空闲超时触发)。

  4. 地址族校验:返回数据包必须与原始连接的地址族(IPv4 或 IPv6)匹配。混合地址族的数据包会被静默丢弃。

  5. 原始数据包校验和:IP 和 UDP 校验和必须正确计算。IPv4 需要头部校验和 + 伪头部 UDP 校验和;IPv6 仅需伪头部 UDP 校验和。

  6. 逐包寻址:Buffer.UDP 字段允许 XUDP 响应中每个数据包拥有不同的源地址。这对于服务器从不同于客户端发送地址进行响应的协议至关重要。

用于重新实现目的的技术分析。