Skip to content

UDP Full-Cone NAT

يُنفّذ معالج TUN آلية Full-Cone NAT لحركة مرور UDP، مما يسمح لحزم الإرجاع من أي عنوان بعيد بالوصول إلى العميل الأصلي — وليس فقط من العنوان الذي أُرسلت إليه الحزمة في الأصل.

المصدر: proxy/tun/udp_fullcone.go

شرح أنواع NAT

Symmetric NAT:       Client:1234 → Server_A:80  ✓
                     Server_B:80 → Client:1234  ✗ (different server, blocked)

Full-Cone NAT:       Client:1234 → Server_A:80  ✓
                     Server_B:80 → Client:1234  ✓ (any server can respond)

Full-Cone NAT مطلوب لـ:

  • تطبيقات WebRTC / P2P
  • خوادم الألعاب (المبنية على UDP)
  • بروتوكولات STUN/TURN
  • أي بروتوكول تأتي فيه الاستجابة من عنوان مختلف

البنية المعمارية

mermaid
flowchart LR
    subgraph TUN["TUN Device"]
        App([Application])
    end

    subgraph UDPHandler["UDP Connection Handler"]
        Map["Connection Map<br/>src → udpConn"]
        Conn1["udpConn A<br/>(src=10.0.0.1:5000)"]
        Conn2["udpConn B<br/>(src=10.0.0.1:6000)"]
    end

    subgraph Xray["Xray Routing"]
        Handler["TUN Handler"]
        Dispatcher["Dispatcher"]
    end

    App -->|"UDP packet<br/>src=10.0.0.1:5000<br/>dst=8.8.8.8:53"| UDPHandler
    UDPHandler -->|"lookup by src"| Map
    Map -->|"new conn"| Conn1
    Conn1 -->|"HandleConnection()"| Handler
    Handler -->|"DispatchLink()"| Dispatcher

    Dispatcher -->|"response packet"| Conn1
    Conn1 -->|"writeRawUDPPacket()"| App

معالج الاتصال

go
type udpConnectionHandler struct {
    sync.Mutex
    udpConns map[net.Destination]*udpConn  // keyed by SOURCE address
    handleConnection func(conn net.Conn, dest net.Destination)
    writePacket      func(data []byte, src, dst net.Destination) error
}

الفكرة الأساسية: يتم ربط الاتصالات بواسطة عنوان المصدر فقط، وليس بواسطة زوج (المصدر، الوجهة). هذا ما يجعلها Full-Cone — يمكن لأي خادم الرد إلى نفس المصدر.

استقبال الحزم

go
func (u *udpConnectionHandler) HandlePacket(src, dst net.Destination, data []byte) bool {
    u.Lock()
    conn, found := u.udpConns[src]
    if !found {
        // New source: create connection
        egress := make(chan []byte, 16)
        conn = &udpConn{handler: u, egress: egress, src: src, dst: dst}
        u.udpConns[src] = conn

        // Dispatch to Xray routing (in goroutine)
        go u.handleConnection(conn, dst)
    }
    u.Unlock()

    // Forward packet data to the connection's egress channel
    select {
    case conn.egress <- data:  // delivered
    default:                    // channel full, discard
    }
    return true
}

اتصال UDP الافتراضي

go
type udpConn struct {
    handler *udpConnectionHandler
    egress  chan []byte        // incoming packets (from TUN)
    src     net.Destination    // client source address
    dst     net.Destination    // original destination
}

يُنفّذ udpConn واجهة net.Conn للاستخدام مع موزع Xray:

go
// Read: receive packets from the egress channel
func (c *udpConn) Read(p []byte) (int, error) {
    data, ok := <-c.egress
    if !ok { return 0, io.EOF }
    return copy(p, data), nil
}

// Write: construct raw UDP packet back to source
func (c *udpConn) Write(p []byte) (int, error) {
    // REVERSE src/dst: response goes from dst → src
    err := c.handler.writePacket(p, c.dst, c.src)
    return len(p), err
}

WriteMultiBuffer (مع وجهة لكل حزمة)

go
func (c *udpConn) WriteMultiBuffer(mb buf.MultiBuffer) error {
    for _, b := range mb {
        dst := c.dst
        if b.UDP != nil {
            dst = *b.UDP  // Use per-packet destination from XUDP
        }
        // Validate address family matches
        if dst.Address.Family() != c.dst.Address.Family() {
            continue
        }
        // Send reversed: dst→src
        c.handler.writePacket(b.Bytes(), dst, c.src)
    }
    return nil
}

يسمح حقل b.UDP لبروتوكول XUDP بتحديد وجهة مختلفة لكل حزمة (Full-Cone: الإرجاع من أي عنوان).

بناء الحزم الخام

يجب بناء حزم UDP العائدة كحزم IP خام (نظرًا لعدم استخدام معيد توجيه UDP في gVisor):

IPv4 Packet:
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| Ver|IHL|  TOS  |         Total Length           |    TTL=64  |
| Protocol=17(UDP)|  Header Checksum  |   Source IP (4 bytes)  |
|  Destination IP (4 bytes)  |                                  |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
UDP Header:
+--+--+--+--+--+--+--+--+
| Src Port  | Dst Port  |
|  Length    | Checksum  |
+--+--+--+--+--+--+--+--+
|       Payload          |
+--+--+--+--+--+--+--+--+

يتم حقن الحزمة مرة أخرى في مكدس gVisor عبر WriteRawPacket()، والذي:

  1. يعالجها عبر طبقة الشبكة في gVisor
  2. يُسلّمها إلى نقطة نهاية TUN
  3. يُرسلها جهاز TUN إلى النواة
  4. تُسلّمها النواة إلى التطبيق

دورة حياة الاتصال

mermaid
sequenceDiagram
    participant App as Application
    participant TUN
    participant UH as UDP Handler
    participant UC as udpConn
    participant Xray as Xray Dispatcher

    App->>TUN: UDP packet (src=A, dst=B)
    TUN->>UH: HandlePacket(A, B, data)
    UH->>UH: udpConns[A] not found
    UH->>UC: Create udpConn(src=A, dst=B)
    UH-->>Xray: go HandleConnection(conn, B)
    UH->>UC: egress <- data

    Xray->>UC: Read() [blocks on egress channel]
    UC-->>Xray: data
    Xray->>Xray: Route + forward to outbound

    Note over Xray: Response arrives (possibly from C, not B)

    Xray->>UC: Write(response) or WriteMultiBuffer
    UC->>UH: writeRawUDPPacket(response, B→A)
    UH->>TUN: Raw IP+UDP packet
    TUN->>App: UDP response

    App->>TUN: Another packet (src=A, dst=D)
    TUN->>UH: HandlePacket(A, D, data)
    UH->>UH: udpConns[A] exists!
    UH->>UC: egress <- data (reuse connection)

    Note over UC: Connection closed when Xray finishes
    UC->>UH: connectionFinished(A)
    UH->>UH: delete udpConns[A]

ملاحظات التنفيذ

  1. خريطة مفهرسة بالمصدر: خريطة الاتصالات مفهرسة بالمصدر فقط (map[net.Destination]*udpConn). هذا هو Full-Cone: اتصال واحد لكل مصدر عميل، بغض النظر عن الوجهة.

  2. تخزين القناة المؤقت: سعة قناة egress هي 16. إذا امتلأت، يتم إسقاط الحزم. هذا يمنع مشاكل الذاكرة لكن قد يفقد حزم UDP تحت الحمل المفاجئ.

  3. لا يوجد تنظيف بالمهلة الزمنية: لا يوجد تنظيف صريح لاتصالات UDP الخاملة بناءً على المهلة الزمنية. يتم تنظيف الاتصال عندما ينتهي موزع Xray (يُفعّل بواسطة مهلة خمول الموزع).

  4. التحقق من عائلة العنوان: يجب أن تتطابق حزم الإرجاع مع عائلة العنوان (IPv4 أو IPv6) للاتصال الأصلي. الحزم المختلطة العائلة يتم إسقاطها بصمت.

  5. مجاميع تحقق الحزم الخام: يجب حساب مجاميع تحقق IP وUDP بشكل صحيح. يحتاج IPv4 إلى مجموع تحقق الترويسة + مجموع تحقق UDP بالترويسة الزائفة. يحتاج IPv6 فقط إلى مجموع تحقق UDP بالترويسة الزائفة.

  6. العنونة لكل حزمة: يسمح حقل Buffer.UDP لاستجابات XUDP بأن يكون لها عناوين مصدر مختلفة لكل حزمة. هذا ضروري للبروتوكولات التي يستجيب فيها الخادم من عنوان مختلف عن العنوان الذي أرسل إليه العميل.

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