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 响应中每个数据包拥有不同的源地址。这对于服务器从不同于客户端发送地址进行响应的协议至关重要。

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