WireGuard
يدمج Xray-core بروتوكول WireGuard كبروتوكول صادر ووارد، مستخدماً تنفيذ wireguard-go في مساحة المستخدم ومكدس شبكة gVisor (netstack) لإنشاء واجهات نفق افتراضية بالكامل في مساحة المستخدم. يسمح هذا بنفق WireGuard دون الحاجة إلى صلاحيات جهاز TUN على مستوى نظام التشغيل (على معظم المنصات).
نظرة عامة
- الاتجاه: وارد + صادر
- النقل: UDP (يعمل بروتوكول WireGuard عبر UDP)
- التشفير: إطار عمل بروتوكول Noise (Curve25519, ChaCha20-Poly1305, BLAKE2s)
- المصادقة: أزواج مفاتيح عامة/خاصة + مفتاح مُشترك مسبق اختياري
- UDP: دعم كامل (عبر مكدس الشبكة في مساحة المستخدم)
- TCP: دعم كامل (عبر مكدس الشبكة في مساحة المستخدم)
- TUN النواة: مدعوم على Linux (اختياري، معطّل افتراضياً للوارد)
البنية المعمارية
graph TD
subgraph "Xray-core Process"
A[Outbound Handler] --> B[gVisor netstack]
B --> C[wireguard-go Device]
C --> D[netBindClient]
D --> E[internet.Dialer]
end
E --> F[WireGuard Peer UDP]
subgraph "Inbound (Server Mode)"
G[UDP Listener] --> H[netBindServer]
H --> I[wireguard-go Device]
I --> J[gVisor netstack]
J --> K[TCP/UDP Forwarder]
K --> L[Routing Dispatcher]
endالمكونات الرئيسية
| المكون | الملف | الغرض |
|---|---|---|
Handler (صادر) | proxy/wireguard/client.go | نفق WireGuard الصادر |
Server (وارد) | proxy/wireguard/server.go | نقطة نهاية WireGuard الواردة |
واجهة Tunnel | proxy/wireguard/tun.go | جهاز TUN مجرد |
gvisorNet | proxy/wireguard/tun.go | TUN افتراضي مبني على gVisor |
netBindClient | proxy/wireguard/bind.go | نقل UDP من جانب العميل |
netBindServer | proxy/wireguard/bind.go | نقل UDP من جانب الخادم |
حزمة gvisortun | proxy/wireguard/gvisortun/ | إعداد مكدس شبكة gVisor |
واجهة النفق
type Tunnel interface {
BuildDevice(ipc string, bind conn.Bind) error
DialContextTCPAddrPort(ctx context.Context, addr netip.AddrPort) (net.Conn, error)
DialUDPAddrPort(laddr, raddr netip.AddrPort) (net.Conn, error)
Close() error
}المصدر: proxy/wireguard/tun.go:33-38
تُجرّد واجهة Tunnel جهاز الشبكة الافتراضي. بعد استدعاء BuildDevice() مع سلسلة إعدادات IPC، يوفر النفق واجهات Go القياسية net.Conn لـ TCP وUDP.
مكدس شبكة gVisor
التنفيذ الأساسي يستخدم مكدس TCP/IP في مساحة المستخدم الخاص بـ gVisor:
func createGVisorTun(localAddresses []netip.Addr, mtu int,
handler promiscuousModeHandler) (Tunnel, error) {
tun, n, stack, err := gvisortun.CreateNetTUN(localAddresses, mtu, handler != nil)
// ...
}المصدر: proxy/wireguard/tun.go:139-144
الوضع المختلط (الخادم)
عند تقديم دالة handler (وضع الخادم)، يُعدّ gVisor مُحوّلات TCP وUDP تلتقط جميع الحزم الواردة:
// TCP Forwarder
tcpForwarder := tcp.NewForwarder(stack, 0, 65535, func(r *tcp.ForwarderRequest) {
ep, err := r.CreateEndpoint(&wq)
r.Complete(false)
ep.SocketOptions().SetKeepAlive(true)
handler(net.TCPDestination(...), gonet.NewTCPConn(&wq, ep))
})
stack.SetTransportProtocolHandler(tcp.ProtocolNumber, tcpForwarder.HandlePacket)
// UDP Forwarder
udpForwarder := udp.NewForwarder(stack, func(r *udp.ForwarderRequest) {
ep, _ := r.CreateEndpoint(&wq)
ep.SocketOptions().SetLinger(tcpip.LingerOption{Enabled: true, Timeout: 15 * time.Second})
handler(net.UDPDestination(...), gonet.NewUDPConn(&wq, ep))
})
stack.SetTransportProtocolHandler(udp.ProtocolNumber, udpForwarder.HandlePacket)المصدر: proxy/wireguard/tun.go:150-201
المعالج الصادر
الملف: proxy/wireguard/client.go
التهيئة
func New(ctx context.Context, conf *DeviceConfig) (*Handler, error) {
endpoints, hasIPv4, hasIPv6, err := parseEndpoints(conf)
return &Handler{
conf: conf,
dns: d,
endpoints: endpoints,
hasIPv4: hasIPv4,
hasIPv6: hasIPv6,
}, nil
}المصدر: proxy/wireguard/client.go:62-78
يُهيّأ جهاز WireGuard بشكل كسول عند أول استخدام ويُعاد إنشاؤه عند تغير المتصل:
func (h *Handler) processWireGuard(ctx context.Context, dialer internet.Dialer) error {
h.wgLock.Lock()
defer h.wgLock.Unlock()
if h.bind != nil && h.bind.dialer == dialer && h.net != nil {
return nil // Already initialized with same dialer
}
// Create new bind and tunnel
h.bind = &netBindClient{
netBind: netBind{dns: h.dns, ...},
ctx: ctx,
dialer: dialer,
reserved: h.conf.Reserved,
}
h.net, err = h.makeVirtualTun()
}المصدر: proxy/wireguard/client.go:94-142
معالجة الاتصالات
func (h *Handler) Process(ctx context.Context, link *transport.Link,
dialer internet.Dialer) error {
// 1. Initialize/reuse WireGuard tunnel
h.processWireGuard(ctx, dialer)
// 2. Resolve DNS if destination is a domain
if addr.Family().IsDomain() {
ips, _, err = h.dns.LookupIP(addr.Domain(), dns.IPOption{
IPv4Enable: h.hasIPv4 && h.conf.preferIP4(),
IPv6Enable: h.hasIPv6 && h.conf.preferIP6(),
})
}
// 3. Dial through the virtual tunnel
if command == protocol.RequestCommandTCP {
conn, err := h.net.DialContextTCPAddrPort(ctx, addrPort)
// Copy data bidirectionally
} else {
conn, err := h.net.DialUDPAddrPort(netip.AddrPort{}, addrPort)
// Copy data bidirectionally
}
}المصدر: proxy/wireguard/client.go:145-252
إعدادات IPC
يُهيّأ جهاز WireGuard عبر سلسلة طلب IPC (بنفس صيغة wg set):
func (h *Handler) createIPCRequest() string {
var request strings.Builder
request.WriteString(fmt.Sprintf("private_key=%s\n", h.conf.SecretKey))
for _, peer := range h.conf.Peers {
request.WriteString(fmt.Sprintf("public_key=%s\n", peer.PublicKey))
if peer.PreSharedKey != "" {
request.WriteString(fmt.Sprintf("preshared_key=%s\n", peer.PreSharedKey))
}
request.WriteString(fmt.Sprintf("endpoint=%s:%s\n", addr, port))
for _, ip := range peer.AllowedIps {
request.WriteString(fmt.Sprintf("allowed_ip=%s\n", ip))
}
if peer.KeepAlive != 0 {
request.WriteString(fmt.Sprintf("persistent_keepalive_interval=%d\n", peer.KeepAlive))
}
}
return request.String()
}المصدر: proxy/wireguard/client.go:272-338
تحليل DNS لنقطة النهاية: إذا كانت نقطة نهاية النظير نطاقاً، يتم تحليلها باستخدام إما IP المُحلّل مسبقاً للمتصل أو عميل DNS:
if addr.Family().IsDomain() {
dialerIp := h.bind.dialer.DestIpAddress()
if dialerIp != nil {
addr = net.ParseAddress(dialerIp.String())
} else {
ips, _, _ = h.dns.LookupIP(addr.Domain(), ...)
addr = net.IPAddress(ips[dice.Roll(len(ips))])
}
}المصدر: proxy/wireguard/client.go:296-321
المعالج الوارد (الخادم)
الملف: proxy/wireguard/server.go
دعم الشبكة
يقبل الخادم اتصالات UDP فقط (بروتوكول نقل WireGuard):
func (*Server) Network() []net.Network {
return []net.Network{net.Network_UDP}
}المصدر: proxy/wireguard/server.go:75-77
معالجة الحزم
يقرأ الخادم حزم UDP من طبقة النقل ويُغذّيها إلى جهاز WireGuard:
func (s *Server) Process(ctx context.Context, network net.Network,
conn stat.Connection, dispatcher routing.Dispatcher) error {
reader := buf.NewPacketReader(conn)
for {
mpayload, err := reader.ReadMultiBuffer()
for _, payload := range mpayload {
v, ok := <-s.bindServer.readQueue
// Feed packet to wireguard-go
v.bytes = payload.Read(v.buff)
v.endpoint = nep
v.waiter.Done()
}
}
}المصدر: proxy/wireguard/server.go:80-120
تحويل الاتصالات
عندما يستقبل مكدس gVisor اتصالات TCP/UDP مفكوكة التشفير من النفق، يوزّعها معالج forwardConnection:
func (s *Server) forwardConnection(dest net.Destination, conn net.Conn) {
defer conn.Close()
link, err := s.info.dispatcher.Dispatch(ctx, dest)
// Bidirectional copy between tunnel conn and dispatched link
task.Run(ctx, requestDone, responseDone)
}المصدر: proxy/wireguard/server.go:122-190
اختيار وضع TUN
الملف: proxy/wireguard/config.go
يختار المعالج بين TUN النواة وgVisor TUN:
func (c *DeviceConfig) createTun() tunCreator {
if !c.IsClient {
return createGVisorTun // Server always uses gVisor
}
if c.NoKernelTun {
return createGVisorTun
}
if kernelTunSupported, _ := KernelTunSupported(); !kernelTunSupported {
return createGVisorTun
}
return createKernelTun // Linux only
}المصدر: proxy/wireguard/config.go:33-54
TUN النواة (Linux)
الملف: proxy/wireguard/tun_linux.go
على Linux مع الصلاحيات المناسبة، يمكن استخدام جهاز TUN حقيقي للنواة لأداء أفضل. تتعامل النواة مع معالجة TCP/IP بشكل أصلي.
TUN الافتراضي (المنصات الأخرى)
الملف: proxy/wireguard/tun_default.go
على المنصات غير Linux، يتوفر gVisor TUN فقط.
استراتيجية النطاق
يدعم WireGuard استراتيجيات تحليل DNS مع احتياطي:
| الاستراتيجية | التفضيل | الاحتياطي |
|---|---|---|
FORCE_IP | IPv4 + IPv6 | بدون |
FORCE_IP4 | IPv4 فقط | بدون |
FORCE_IP6 | IPv6 فقط | بدون |
FORCE_IP46 | IPv4 | IPv6 |
FORCE_IP64 | IPv6 | IPv4 |
المصدر: proxy/wireguard/config.go:9-31
ملاحظات التنفيذ
التهيئة الكسولة: لا يُنشأ نفق WireGuard وقت التهيئة. يُهيّأ عند أول استدعاء
Process()ويُعاد استخدامه للاتصالات اللاحقة. إذا تغير المتصل (مثل توجيه صادر مختلف)، يُعاد إنشاء النفق.البايتات المحجوزة: يُعيّن حقل
Reservedفي الإعدادات 3 بايتات في ترويسة UDP الخاصة بـ WireGuard، تُستخدم من قبل بعض مقدمي الخدمات (مثل Cloudflare WARP) للتوجيه.
المصدر: proxy/wireguard/client.go:129
أمان الخيوط: يستخدم المعالج
sync.Mutex(wgLock) لحماية إنشاء وتبديل النفق. تتشارك عدة اتصالات متزامنة نفس النفق.دورة حياة الاتصال: كل اتصال TCP/UDP عبر النفق يُدار بشكل مستقل مع سياسات مهلة Xray القياسية. يستمر النفق نفسه عبر الاتصالات.
keep-alive لـ TCP في gVisor: اتصالات TCP عبر مُحوّل الخادم تُفعّل فيها خاصية keep-alive لمنع الاتصالات المعلّقة:
ep.SocketOptions().SetKeepAlive(true)المصدر: proxy/wireguard/tun.go:168
- مهلة UDP linger: نقاط نهاية UDP من جانب الخادم لها مهلة linger تبلغ 15 ثانية لضمان تحرير الموارد في الوقت المناسب:
ep.SocketOptions().SetLinger(tcpip.LingerOption{Enabled: true, Timeout: 15 * time.Second})المصدر: proxy/wireguard/tun.go:191-193
- بدون تشفير في طبقة Xray: يتعامل WireGuard مع كل التشفير داخلياً عبر بروتوكول Noise. يقوم Xray ببساطة بتمرير حزم IP المفكوكة التشفير عبر مكدس الشبكة الافتراضي. يعكس إعداد
CanSpliceCopy = 3هذا.