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
- أي بروتوكول تأتي فيه الاستجابة من عنوان مختلف
البنية المعمارية
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معالج الاتصال
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 — يمكن لأي خادم الرد إلى نفس المصدر.
استقبال الحزم
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 الافتراضي
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:
// 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 (مع وجهة لكل حزمة)
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()، والذي:
- يعالجها عبر طبقة الشبكة في gVisor
- يُسلّمها إلى نقطة نهاية TUN
- يُرسلها جهاز TUN إلى النواة
- تُسلّمها النواة إلى التطبيق
دورة حياة الاتصال
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]ملاحظات التنفيذ
خريطة مفهرسة بالمصدر: خريطة الاتصالات مفهرسة بالمصدر فقط (
map[net.Destination]*udpConn). هذا هو Full-Cone: اتصال واحد لكل مصدر عميل، بغض النظر عن الوجهة.تخزين القناة المؤقت: سعة قناة egress هي 16. إذا امتلأت، يتم إسقاط الحزم. هذا يمنع مشاكل الذاكرة لكن قد يفقد حزم UDP تحت الحمل المفاجئ.
لا يوجد تنظيف بالمهلة الزمنية: لا يوجد تنظيف صريح لاتصالات UDP الخاملة بناءً على المهلة الزمنية. يتم تنظيف الاتصال عندما ينتهي موزع Xray (يُفعّل بواسطة مهلة خمول الموزع).
التحقق من عائلة العنوان: يجب أن تتطابق حزم الإرجاع مع عائلة العنوان (IPv4 أو IPv6) للاتصال الأصلي. الحزم المختلطة العائلة يتم إسقاطها بصمت.
مجاميع تحقق الحزم الخام: يجب حساب مجاميع تحقق IP وUDP بشكل صحيح. يحتاج IPv4 إلى مجموع تحقق الترويسة + مجموع تحقق UDP بالترويسة الزائفة. يحتاج IPv6 فقط إلى مجموع تحقق UDP بالترويسة الزائفة.
العنونة لكل حزمة: يسمح حقل
Buffer.UDPلاستجابات XUDP بأن يكون لها عناوين مصدر مختلفة لكل حزمة. هذا ضروري للبروتوكولات التي يستجيب فيها الخادم من عنوان مختلف عن العنوان الذي أرسل إليه العميل.