Skip to content

بروتوكول XUDP

يُوسّع XUDP بروتوكول Mux لدعم UDP مع عنونة لكل حزمة، مما يُمكّن سلوك Full-Cone NAT عبر اتصال نقل واحد. يُستخدم بواسطة VLESS وVMess وبروتوكولات أخرى لتوكيل حركة مرور UDP.

المصدر: common/xudp/xudp.go

لماذا XUDP؟

Mux القياسي يُنشئ جلسة واحدة لكل وجهة. بالنسبة لـ UDP:

  • العميل يُرسل إلى Server_A → الجلسة 1
  • العميل يُرسل إلى Server_B → الجلسة 2
  • Server_A يستجيب → الجلسة 1 صحيح
  • Server_C يستجيب → لا جلسة! خطأ

يحل XUDP هذا بـ:

  1. استخدام جلسة واحدة لجميع حركة مرور UDP من مصدر واحد
  2. تضمين عنوان الوجهة في كل حزمة (وليس فقط في ترويسة الجلسة)
  3. دعم الاستجابات من أي عنوان (Full-Cone NAT)

تنسيق السلك

الحزمة الأولى (جلسة جديدة)

+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| Meta Len  | Session=0 | Status=New | Opt=Data      |
| (2B BE)   | (2B: 0x00)| (1B: 0x01) | (1B: 0x01)  |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| Network=UDP | Port    | AddrType | Address         |
| (1B: 0x02)  | (2B BE) | (1B)     | (var)          |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| GlobalID (8 bytes)                                  |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| Data Len  | UDP Payload                             |
| (2B BE)   | (var)                                   |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+

الحزم اللاحقة (متابعة الجلسة)

+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| Meta Len  | Session=0 | Status=Keep | Opt=Data     |
| (2B BE)   | (2B: 0x00)| (1B: 0x02)  | (1B: 0x01) |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| Network=UDP | Port    | AddrType | Address         |
| (1B: 0x02)  | (2B BE) | (1B)     | (var)          |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| Data Len  | UDP Payload                             |
| (2B BE)   | (var)                                   |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+

الفروقات الرئيسية عن Mux القياسي:

  • معرف الجلسة دائمًا 0 (XUDP يستخدم جلسة واحدة)
  • إطارات Keep تتضمن عنوان الوجهة (عنونة لكل حزمة)
  • GlobalID يُرسل فقط في الإطار الأول (New)

PacketWriter

go
type PacketWriter struct {
    Writer   buf.Writer       // underlying mux/transport writer
    Dest     net.Destination  // initial destination
    GlobalID [8]byte          // connection identity
}

func (w *PacketWriter) WriteMultiBuffer(mb buf.MultiBuffer) error {
    for _, b := range mb {
        eb := buf.New()
        eb.Write([]byte{0, 0, 0, 0})  // meta len placeholder + session ID=0

        if w.Dest.Network == net.Network_UDP {
            // First packet: New session
            eb.WriteByte(1)  // Status: New
            eb.WriteByte(1)  // Option: Data
            eb.WriteByte(2)  // Network: UDP
            AddrParser.WriteAddressPort(eb, w.Dest.Address, w.Dest.Port)
            if b.UDP != nil {
                eb.Write(w.GlobalID[:])  // 8-byte GlobalID
            }
            w.Dest.Network = net.Network_Unknown  // switch to Keep for next packet
        } else {
            // Subsequent packets: Keep session
            eb.WriteByte(2)  // Status: Keep
            eb.WriteByte(1)  // Option: Data
            if b.UDP != nil {
                eb.WriteByte(2)  // Network: UDP
                AddrParser.WriteAddressPort(eb, b.UDP.Address, b.UDP.Port)
            }
        }

        // Write meta length
        l := eb.Len() - 2
        eb.SetByte(0, byte(l>>8))
        eb.SetByte(1, byte(l))

        // Write data length + data
        eb.WriteByte(byte(length >> 8))
        eb.WriteByte(byte(length))
        eb.Write(b.Bytes())

        mb2Write = append(mb2Write, eb)
    }
    return w.Writer.WriteMultiBuffer(mb2Write)
}

PacketReader

go
type PacketReader struct {
    Reader io.Reader
    cache  []byte  // 2-byte buffer for length reads
}

func (r *PacketReader) ReadMultiBuffer() (buf.MultiBuffer, error) {
    for {
        // Read meta length (2 bytes)
        io.ReadFull(r.Reader, r.cache)
        l := int32(r.cache[0])<<8 | int32(r.cache[1])

        // Read metadata
        b := buf.New()
        b.ReadFullFrom(r.Reader, l)

        switch b.Byte(2) {  // Status byte
        case 2:  // Keep
            if l > 4 && b.Byte(4) == 2 {  // Has UDP address
                b.Advance(5)
                addr, port, _ := AddrParser.ReadAddressPort(nil, b)
                b.UDP = &net.Destination{
                    Network: net.Network_UDP,
                    Address: addr,
                    Port:    port,
                }
            }
        case 4:  // KeepAlive (discard)
            discard = true
        default:  // End
            return nil, io.EOF
        }

        // Read data length + payload
        b.Clear()
        if b.Byte(3) == 1 {  // Option: Data
            io.ReadFull(r.Reader, r.cache)
            length := int32(r.cache[0])<<8 | int32(r.cache[1])
            b.ReadFullFrom(r.Reader, length)
            if !discard {
                return buf.MultiBuffer{b}, nil
            }
        }
    }
}

GlobalID

يُعرّف GlobalID "اتصال" UDP الخاص بالعميل لـ Full-Cone NAT على جانب الخادم:

go
func GetGlobalID(ctx context.Context) (globalID [8]byte) {
    // Only generate for cone mode
    if cone := ctx.Value("cone"); cone == nil || !cone.(bool) {
        return  // zero ID: no cone NAT
    }

    // Only for supported inbounds
    inbound := session.InboundFromContext(ctx)
    if inbound != nil && inbound.Source.Network == net.Network_UDP &&
        (inbound.Name == "dokodemo-door" ||
         inbound.Name == "socks" ||
         inbound.Name == "shadowsocks" ||
         inbound.Name == "tun") {

        // BLAKE3 hash of source address
        h := blake3.New(8, BaseKey)
        h.Write([]byte(inbound.Source.String()))
        copy(globalID[:], h.Sum(nil))
    }
    return
}

كيف يعمل GlobalID

  1. العميل A (المصدر=10.0.0.1:5000) يُرسل UDP → الخادم يُجزّئ "10.0.0.1:5000" → GlobalID abc123
  2. الخادم يُنشئ جلسة cone مفهرسة بـ GlobalID abc123
  3. أي حزمة UDP عائدة مع GlobalID abc123 تُوجّه إلى 10.0.0.1:5000
  4. عميل مختلف B (المصدر=10.0.0.2:3000) → GlobalID مختلف → جلسة منفصلة

BaseKey

مفتاح BLAKE3 يُولّد عشوائيًا عند بدء التشغيل:

go
BaseKey = make([]byte, 32)
rand.Read(BaseKey)

أو يُهيّأ عبر متغير البيئة XRAY_XUDP_BASEKEY (لسلوك قابل للتكرار، مثلاً عبر إعادات التشغيل).

XUDP في مخرج VLESS

عندما يتعامل مخرج VLESS مع UDP مع cone NAT:

go
// Convert UDP to mux+XUDP
if command == UDP && (flow == XRV || cone) {
    request.Command = Mux
    request.Address = "v1.mux.cool"
    request.Port = 666  // magic port: indicates XUDP
}

// Wrap writer with XUDP framing
serverWriter = xudp.NewPacketWriter(serverWriter, target, xudp.GetGlobalID(ctx))

// Wrap reader with XUDP deframing
serverReader = xudp.NewPacketReader(conn)

كشف XUDP على الخادم

يكشف مدخل VLESS عن XUDP بفحص أول إطار Mux:

go
func isMuxAndNotXUDP(request, first) bool {
    if request.Command != Mux { return false }
    if first.Len() < 7 { return true }  // assume regular mux

    firstBytes := first.Bytes()
    // XUDP: session=0 (bytes 2,3), network=UDP (byte 6)
    return !(firstBytes[2] == 0 &&
             firstBytes[3] == 0 &&
             firstBytes[6] == 2)
}

إذا تم الكشف عن XUDP، يستخدم الخادم قارئ حزم متوافق مع XUDP بدلاً من عامل خادم Mux العادي.

مثال تدفق البيانات

Client sends DNS query to 8.8.8.8:53:

PacketWriter writes:
  [00 0C]             # meta length = 12
  [00 00]             # session ID = 0
  [01]                # status = New
  [01]                # option = Data
  [02]                # network = UDP
  [00 35]             # port = 53
  [01]                # addr type = IPv4
  [08 08 08 08]       # address = 8.8.8.8
  [a1 b2 c3 d4 e5 f6 g7 h8]  # GlobalID (8 bytes)
  [00 2A]             # data length = 42
  [... DNS query ...]  # payload

Response arrives from 8.8.8.8:53:

PacketReader reads:
  [00 0C]             # meta length
  [00 00]             # session ID = 0
  [02]                # status = Keep
  [01]                # option = Data
  [02]                # network = UDP
  [00 35]             # port = 53
  [01 08 08 08 08]    # addr = 8.8.8.8
  [00 XX]             # data length
  [... DNS response ...]
  → b.UDP = {Network: UDP, Address: 8.8.8.8, Port: 53}

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

  1. معرف الجلسة 0 خاص: في XUDP، معرف الجلسة دائمًا 0. Mux العادي يبدأ معرفات الجلسات من 1.

  2. GlobalID لكل مصدر: كل عنوان مصدر عميل يحصل على GlobalID فريد. يستخدم الخادم هذا للحفاظ على حالة cone NAT لكل عميل.

  3. العنونة لكل حزمة: كل إطار Keep يمكن أن يحمل عنوان وجهة مختلف. هذا ما يُمكّن Full-Cone NAT — يمكن أن تأتي الاستجابة من أي عنوان.

  4. نشر Buffer.UDP: حقل Buffer.UDP يتدفق عبر خط الأنابيب بالكامل: قارئ XUDP يُعيّنه، الأنبوب يحافظ عليه، كاتب XUDP يقرأه. لا تُزل هذا الحقل أبدًا.

  5. وضع Cone قابل للتهيئة: قيمة السياق cone (من الإعداد) تتحكم في ما إذا كان يُستخدم XUDP. عند التعطيل، كل وجهة UDP تحصل على جلسة Mux منفصلة (سلوك Symmetric NAT).

  6. BLAKE3 للتجزئة: تجزئة 8 بايت توفر تفردًا كافيًا مع كونها مدمجة. BaseKey يمنع العملاء من تخمين GlobalIDs بعضهم البعض.

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