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 على الطبقة الثالثة (حزم 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)

بنية المكدس

mermaid
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

إنشاء المكدس

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
}

نقطة نهاية الرابط

نقطة نهاية الرابط هي الجسر بين واصف ملف 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

معالجة 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 ميجابايت للاستقبال + 6 ميجابايت للإرسال لكل اتصال)
  • مخازن الحزم للحزم قيد النقل
  • حالة البروتوكول (أرقام تسلسل 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 يسبب تجزئة أو إسقاط.

تحليل تقني لأغراض إعادة التنفيذ.