مكدس 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 على الطبقة الثالثة (حزم IP)، لكن بروتوكولات الوكيل في Xray-core تعمل على الطبقة الرابعة وما فوقها (تدفقات TCP، مخططات بيانات UDP). يسد مكدس IP في فضاء المستخدم هذه الفجوة:
TUN device → Raw IP packets (L3)
gVisor TCP/IP stack → TCP connections, UDP packets (L4)
Xray Handler → Application connections (L7)بنية المكدس
flowchart TB
subgraph TUN["TUN Device (Kernel)"]
FD["File Descriptor"]
end
subgraph Endpoint["Link Endpoint"]
RX["Read Loop:<br/>TUN fd → gVisor"]
TX["Write: gVisor → TUN fd"]
end
subgraph gVisor["gVisor Stack"]
NIC["NIC (Network Interface)"]
IPv4["IPv4 Protocol"]
IPv6["IPv6 Protocol"]
TCP["TCP Protocol"]
UDP["UDP Protocol"]
TCPFwd["TCP Forwarder"]
UDPHandler["UDP Handler"]
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 -->|"raw packet data"| UDPConn["UDP Connection Handler"]
gVisor -->|"response packets"| 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
}نقطة نهاية الرابط
نقطة نهاية الرابط هي الجسر بين واصف ملف 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
- إنشاء نقطة نهاية gVisor (يُنفّذ مصافحة TCP الثلاثية داخليًا)
- تغليف نقطة النهاية في
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 ميجابايت للاستقبال + 6 ميجابايت للإرسال لكل اتصال)
- مخازن الحزم للحزم قيد النقل
- حالة البروتوكول (أرقام تسلسل 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 يسبب تجزئة أو إسقاط.