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)Архитектура стека
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Создание стека
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
// 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
Link endpoint — это мост между файловым дескриптором TUN и обработкой пакетов gVisor:
type tunEndpoint struct {
tun Tun // TUN device
dispatcher stack.NetworkDispatcher // gVisor packet dispatcher
mtu uint32
}Входящий путь (TUN → gVisor)
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)
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-соединения перехватываются форвардером:
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)Форвардер:
- Получает SYN-пакет
- Создаёт endpoint gVisor (выполняет трёхстороннее TCP-рукопожатие внутри)
- Оборачивает endpoint в
gonet.NewTCPConn()(реализуетnet.Conn) - Передаёт обработчику Xray
Обработка UDP
UDP не использует форвардер gVisor. Вместо этого пакеты перехватываются на уровне обработчика транспортного протокола:
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:
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) помогает, начиная с малых размеров и увеличивая их по мере необходимости.
Замечания по реализации
gVisor опционален: Можно использовать более простой подход (lwIP, smoltcp), но gVisor обеспечивает наиболее полную реализацию TCP (SACK, CUBIC, корректная ретрансмиссия и т.д.).
Spoofing + Promiscuous обязательны: Без них gVisor отклоняет пакеты, не адресованные известному IP. Как прокси, любой IP назначения является допустимым.
Обходное решение для RACK/TLP: Отключение восстановления RACK/TLP (
TCPRecovery(0)) — это обходное решение для бага gVisor, при котором соединения зависают под высокой нагрузкой. Следует отслеживать, исправлено ли это в более новых версиях gVisor.UDP через необработанные пакеты: Пользовательская обработка UDP (в обход UDP-форвардера gVisor) необходима для Full-Cone NAT. Конструирование необработанных пакетов (IP+UDP-заголовки, контрольные суммы) должно быть корректным, иначе пакеты будут отброшены.
MTU имеет значение: MTU TUN (по умолчанию 1500) влияет на максимальный размер пакета. MSS вычисляется из MTU. Несовпадение MTU вызывает фрагментацию или отбрасывание пакетов.