Skip to content

gVisor IP 协议栈

gVisor(gvisor.dev/gvisor)提供了完整的用户空间 TCP/IP 协议栈实现。Xray-core 使用它将来自 TUN 接口的原始 IP 数据包处理为应用层连接。

源码proxy/tun/stack_gvisor.goproxy/tun/stack_gvisor_endpoint.go

为什么选择 gVisor?

TUN 设备工作在第 3 层(IP 数据包),而 Xray-core 的代理协议工作在第 4 层及以上(TCP 流、UDP 数据报)。用户空间 IP 协议栈正是弥合这一差距的桥梁:

TUN 设备            → 原始 IP 数据包 (L3)
gVisor TCP/IP 协议栈 → TCP 连接、UDP 数据包 (L4)
Xray Handler        → 应用层连接 (L7)

协议栈架构

mermaid
flowchart TB
    subgraph TUN["TUN 设备(内核)"]
        FD["文件描述符"]
    end

    subgraph Endpoint["Link Endpoint"]
        RX["读取循环:<br/>TUN fd → gVisor"]
        TX["写入: gVisor → TUN fd"]
    end

    subgraph gVisor["gVisor 协议栈"]
        NIC["NIC(网络接口)"]
        IPv4["IPv4 协议"]
        IPv6["IPv6 协议"]
        TCP["TCP 协议"]
        UDP["UDP 协议"]
        TCPFwd["TCP 转发器"]
        UDPHandler["UDP 处理器"]
    end

    FD --> RX
    RX --> NIC
    NIC --> IPv4
    NIC --> IPv6
    IPv4 --> TCP
    IPv4 --> UDP
    IPv6 --> TCP
    IPv6 --> UDP
    TCP --> TCPFwd
    UDP --> UDPHandler

    TCPFwd -->|"gonet.TCPConn"| Handler["Xray TUN Handler"]
    UDPHandler -->|"原始数据包数据"| UDPConn["UDP 连接处理器"]

    gVisor -->|"响应数据包"| TX
    TX --> FD

协议栈创建

go
func createStack(ep stack.LinkEndpoint) (*stack.Stack, error) {
    gStack := stack.New(stack.Options{
        NetworkProtocols: []stack.NetworkProtocolFactory{
            ipv4.NewProtocol,   // IPv4 support
            ipv6.NewProtocol,   // IPv6 support
        },
        TransportProtocols: []stack.TransportProtocolFactory{
            tcp.NewProtocol,    // TCP support
            udp.NewProtocol,    // UDP support
        },
        HandleLocal: false,     // Don't special-case local addresses
    })

    // Create virtual NIC bound to our endpoint
    gStack.CreateNIC(1, ep)

    // Accept ALL destination IPs (route everything through this NIC)
    gStack.SetRouteTable([]tcpip.Route{
        {Destination: header.IPv4EmptySubnet, NIC: 1},  // 0.0.0.0/0
        {Destination: header.IPv6EmptySubnet, NIC: 1},  // ::/0
    })

    // Critical: accept packets for any IP (we're a proxy, not a host)
    gStack.SetSpoofing(1, true)
    gStack.SetPromiscuousMode(1, true)
}

TCP 调优

go
// Congestion control: CUBIC (standard)
gStack.SetTransportProtocolOption(tcp.ProtocolNumber,
    &tcpip.CongestionControlOption("cubic"))

// Selective ACK (improves recovery from packet loss)
gStack.SetTransportProtocolOption(tcp.ProtocolNumber,
    &tcpip.TCPSACKEnabled(true))

// Moderate receive buffer (auto-tune buffer sizes)
gStack.SetTransportProtocolOption(tcp.ProtocolNumber,
    &tcpip.TCPModerateReceiveBufferOption(true))

// Disable RACK/TLP (workaround for gVisor stall bug)
gStack.SetTransportProtocolOption(tcp.ProtocolNumber,
    &tcpip.TCPRecovery(0))

// Buffer sizes
tcpRXBufOpt := tcpip.TCPReceiveBufferSizeRangeOption{
    Min: 4096, Default: 212992, Max: 8388608,  // 4KB → 208KB → 8MB
}
tcpTXBufOpt := tcpip.TCPSendBufferSizeRangeOption{
    Min: 4096, Default: 212992, Max: 6291456,  // 4KB → 208KB → 6MB
}

Link Endpoint 是 TUN 文件描述符与 gVisor 数据包处理之间的桥梁:

go
type tunEndpoint struct {
    tun        Tun                        // TUN device
    dispatcher stack.NetworkDispatcher    // gVisor packet dispatcher
    mtu        uint32
}

入站路径(TUN → gVisor)

go
func (ep *tunEndpoint) dispatchLoop() {
    for {
        // Read raw IP packet from TUN fd
        packet := readFromTUN()

        // Determine IP version from first nibble
        var protocol tcpip.NetworkProtocolNumber
        switch packet[0] >> 4 {
        case 4: protocol = header.IPv4ProtocolNumber
        case 6: protocol = header.IPv6ProtocolNumber
        }

        // Create PacketBuffer and deliver to gVisor
        pkt := stack.NewPacketBuffer(stack.PacketBufferOptions{
            Payload: buffer.MakeWithData(packet),
        })
        ep.dispatcher.DeliverNetworkPacket(protocol, pkt)
    }
}

出站路径(gVisor → TUN)

go
func (ep *tunEndpoint) WritePackets(pkts stack.PacketBufferList) (int, tcpip.Error) {
    for _, pkt := range pkts {
        // Serialize gVisor packet to bytes
        data := pkt.ToView().AsSlice()
        // Write to TUN fd
        ep.tun.Write(data)
    }
}

TCP 转发器

所有 TCP 连接都由转发器拦截:

go
tcpForwarder := tcp.NewForwarder(ipStack,
    0,      // receive buffer size (0 = use default)
    65535,  // max in-flight connections
    func(r *tcp.ForwarderRequest) {
        go handleTCPConnection(r)
    },
)
ipStack.SetTransportProtocolHandler(tcp.ProtocolNumber, tcpForwarder.HandlePacket)

转发器的工作流程:

  1. 接收 SYN 数据包
  2. 创建 gVisor 端点(内部完成 TCP 三次握手)
  3. 将端点包装为 gonet.NewTCPConn()(实现 net.Conn 接口)
  4. 传递给 Xray Handler

UDP 处理

UDP 不使用 gVisor 的转发器,而是在传输层协议处理器级别拦截数据包:

go
ipStack.SetTransportProtocolHandler(udp.ProtocolNumber,
    func(id stack.TransportEndpointID, pkt *stack.PacketBuffer) bool {
        data := pkt.Data().AsRange().ToSlice()

        src := net.UDPDestination(
            net.IPAddress(id.RemoteAddress.AsSlice()),
            net.Port(id.RemotePort),
        )
        dst := net.UDPDestination(
            net.IPAddress(id.LocalAddress.AsSlice()),
            net.Port(id.LocalPort),
        )

        return udpForwarder.HandlePacket(src, dst, data)
    },
)

为什么不使用 gVisor 的 UDP 转发器? 因为 gVisor 的转发器会为每个目的地址创建独立连接,这不支持 Full-Cone NAT(即来自任意地址的返回数据包都应被接受)。

原始 UDP 返回路径

对于 UDP 响应,Xray 必须构造原始 IP+UDP 数据包并注入回 gVisor 协议栈:

go
func (t *stackGVisor) writeRawUDPPacket(payload, src, dst) error {
    // Build UDP header
    udpHdr := header.UDP(...)
    udpHdr.Encode(&header.UDPFields{
        SrcPort: src.Port,
        DstPort: dst.Port,
        Length:  udpLen,
    })
    // Calculate checksum
    udpHdr.SetChecksum(...)

    // Build IP header (v4 or v6)
    if isIPv4 {
        ipHdr := header.IPv4(...)
        ipHdr.Encode(&header.IPv4Fields{
            TotalLength: ...,
            TTL: 64,
            Protocol: header.UDPProtocolNumber,
            SrcAddr: srcIP,
            DstAddr: dstIP,
        })
        ipHdr.SetChecksum(...)
    }

    // Inject packet back into the stack
    t.stack.WriteRawPacket(defaultNIC, ipProtocol, packetData)
}

这个原始数据包通过 gVisor 协议栈返回到 TUN 设备,再传递给原始应用程序。

内存注意事项

gVisor 会为以下内容分配内存:

  • 每个连接的 TCP 缓冲区(每个连接最多 8MB 接收 + 6MB 发送)
  • 在途数据包的包缓冲区
  • 协议状态(TCP 序列号、定时器等)

对于处理数千连接的代理来说,这些内存开销可能非常可观。缓冲区自动调优(TCPModerateReceiveBufferOption)通过从小缓冲区开始、按需增长来缓解这一问题。

实现要点

  1. gVisor 是可选的:你也可以使用更简单的方案(lwIP、smoltcp),但 gVisor 提供了最完整的 TCP 实现(SACK、CUBIC、正确的重传机制等)。

  2. Spoofing + Promiscuous 模式是必须的:如果不启用,gVisor 会拒绝不是发往已知 IP 的数据包。作为代理,每个目的 IP 都是有效的。

  3. RACK/TLP 变通方案:禁用 RACK/TLP 恢复机制(TCPRecovery(0))是针对 gVisor 在高负载下连接卡顿 bug 的变通方案。需关注新版 gVisor 是否已修复此问题。

  4. 通过原始数据包处理 UDP:自定义 UDP 处理(绕过 gVisor 的 UDP 转发器)是 Full-Cone NAT 所必需的。原始数据包的构造(IP+UDP 头部、校验和)必须正确,否则数据包会被丢弃。

  5. MTU 很重要:TUN 的 MTU(默认 1500)影响最大数据包大小。MSS 由 MTU 推导得出。MTU 不匹配会导致分片或丢包。

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