Skip to content

IP-стек gVisor

gVisor (gvisor.dev/gvisor) предоставляет полную пользовательскую реализацию TCP/IP-стека. Xray-core использует его для преобразования необработанных IP-пакетов с TUN-интерфейса в соединения прикладного уровня.

Исходный код: proxy/tun/stack_gvisor.go, proxy/tun/stack_gvisor_endpoint.go

Зачем gVisor?

TUN-устройство работает на уровне 3 (IP-пакеты), а прокси-протоколы Xray-core работают на уровне 4+ (TCP-потоки, UDP-датаграммы). Пользовательский IP-стек устраняет этот разрыв:

TUN-устройство         → Необработанные IP-пакеты (L3)
TCP/IP-стек gVisor     → TCP-соединения, UDP-пакеты (L4)
Обработчик Xray        → Соединения прикладного уровня (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["Обработчик TUN Xray"]
    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. Создаёт endpoint gVisor (выполняет трёхстороннее TCP-рукопожатие внутри)
  3. Оборачивает endpoint в gonet.NewTCPConn() (реализует net.Conn)
  4. Передаёт обработчику Xray

Обработка 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)
    },
)

Почему не используется UDP-форвардер gVisor? Потому что форвардер 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-буферов на каждое соединение (до 8 МБ RX + 6 МБ TX на соединение)
  • Буферов пакетов для транзитных пакетов
  • Состояния протокола (номера последовательности TCP, таймеры и т.д.)

Для прокси, обрабатывающего тысячи соединений, это может быть значительным. Автонастройка буферов (TCPModerateReceiveBufferOption) помогает, начиная с малых размеров и увеличивая их по мере необходимости.

Замечания по реализации

  1. gVisor опционален: Можно использовать более простой подход (lwIP, smoltcp), но gVisor обеспечивает наиболее полную реализацию TCP (SACK, CUBIC, корректная ретрансмиссия и т.д.).

  2. Spoofing + Promiscuous обязательны: Без них gVisor отклоняет пакеты, не адресованные известному IP. Как прокси, любой IP назначения является допустимым.

  3. Обходное решение для RACK/TLP: Отключение восстановления RACK/TLP (TCPRecovery(0)) — это обходное решение для бага gVisor, при котором соединения зависают под высокой нагрузкой. Следует отслеживать, исправлено ли это в более новых версиях gVisor.

  4. UDP через необработанные пакеты: Пользовательская обработка UDP (в обход UDP-форвардера gVisor) необходима для Full-Cone NAT. Конструирование необработанных пакетов (IP+UDP-заголовки, контрольные суммы) должно быть корректным, иначе пакеты будут отброшены.

  5. MTU имеет значение: MTU TUN (по умолчанию 1500) влияет на максимальный размер пакета. MSS вычисляется из MTU. Несовпадение MTU вызывает фрагментацию или отбрасывание пакетов.

Технический анализ для целей повторной реализации.