بروتوكول 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 هذا بـ:
- استخدام جلسة واحدة لجميع حركة مرور UDP من مصدر واحد
- تضمين عنوان الوجهة في كل حزمة (وليس فقط في ترويسة الجلسة)
- دعم الاستجابات من أي عنوان (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
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
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 على جانب الخادم:
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
- العميل A (المصدر=10.0.0.1:5000) يُرسل UDP → الخادم يُجزّئ
"10.0.0.1:5000"→ GlobalIDabc123 - الخادم يُنشئ جلسة cone مفهرسة بـ GlobalID
abc123 - أي حزمة UDP عائدة مع GlobalID
abc123تُوجّه إلى 10.0.0.1:5000 - عميل مختلف B (المصدر=10.0.0.2:3000) → GlobalID مختلف → جلسة منفصلة
BaseKey
مفتاح BLAKE3 يُولّد عشوائيًا عند بدء التشغيل:
BaseKey = make([]byte, 32)
rand.Read(BaseKey)أو يُهيّأ عبر متغير البيئة XRAY_XUDP_BASEKEY (لسلوك قابل للتكرار، مثلاً عبر إعادات التشغيل).
XUDP في مخرج VLESS
عندما يتعامل مخرج VLESS مع UDP مع cone NAT:
// 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:
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}ملاحظات التنفيذ
معرف الجلسة 0 خاص: في XUDP، معرف الجلسة دائمًا 0. Mux العادي يبدأ معرفات الجلسات من 1.
GlobalID لكل مصدر: كل عنوان مصدر عميل يحصل على GlobalID فريد. يستخدم الخادم هذا للحفاظ على حالة cone NAT لكل عميل.
العنونة لكل حزمة: كل إطار Keep يمكن أن يحمل عنوان وجهة مختلف. هذا ما يُمكّن Full-Cone NAT — يمكن أن تأتي الاستجابة من أي عنوان.
نشر Buffer.UDP: حقل
Buffer.UDPيتدفق عبر خط الأنابيب بالكامل: قارئ XUDP يُعيّنه، الأنبوب يحافظ عليه، كاتب XUDP يقرأه. لا تُزل هذا الحقل أبدًا.وضع Cone قابل للتهيئة: قيمة السياق
cone(من الإعداد) تتحكم في ما إذا كان يُستخدم XUDP. عند التعطيل، كل وجهة UDP تحصل على جلسة Mux منفصلة (سلوك Symmetric NAT).BLAKE3 للتجزئة: تجزئة 8 بايت توفر تفردًا كافيًا مع كونها مدمجة. BaseKey يمنع العملاء من تخمين GlobalIDs بعضهم البعض.