بروتوكول WireGuard
يُنفذ WireGuard في sing-box كنقطة نهاية (Endpoint) (وليس كزوج وارد/صادر)، باستخدام مكتبة wireguard-go مع خلفيتي أجهزة: شبكات فضاء المستخدم عبر gVisor أو TUN النظام. تدعم نقطة النهاية حركة المرور الواردة والصادرة، وتغليف جهاز NAT لـ ICMP/ping، وحل DNS للأقران.
المصدر: protocol/wireguard/endpoint.go، transport/wireguard/endpoint.go، transport/wireguard/device.go، transport/wireguard/device_nat.go
بنية نقطة النهاية
يستخدم WireGuard نمط endpoint.Adapter، وهو وارد+صادر مدمج:
type Endpoint struct {
endpoint.Adapter
ctx context.Context
router adapter.Router
dnsRouter adapter.DNSRouter
logger logger.ContextLogger
localAddresses []netip.Prefix
endpoint *wireguard.Endpoint
}ينفذ واجهات متعددة:
var (
_ adapter.OutboundWithPreferredRoutes = (*Endpoint)(nil)
_ dialer.PacketDialerWithDestination = (*Endpoint)(nil)
)دعم الشبكة
يدعم WireGuard بروتوكولات TCP وUDP وICMP:
endpoint.NewAdapterWithDialerOptions(C.TypeWireGuard, tag,
[]string{N.NetworkTCP, N.NetworkUDP, N.NetworkICMP}, options.DialerOptions)واجهة الجهاز
تجرد واجهة Device تنفيذات أنفاق WireGuard المختلفة:
type Device interface {
wgTun.Device // قراءة/كتابة الحزم
N.Dialer // DialContext / ListenPacket
Start() error
SetDevice(device *device.Device)
Inet4Address() netip.Addr
Inet6Address() netip.Addr
}مصنع الأجهزة
يختار مصنع NewDevice التنفيذ بناءً على علامة System:
func NewDevice(options DeviceOptions) (Device, error) {
if !options.System {
return newStackDevice(options) // مكدس فضاء المستخدم gVisor
} else if !tun.WithGVisor {
return newSystemDevice(options) // جهاز TUN النظام
} else {
return newSystemStackDevice(options) // TUN النظام + مكدس gVisor
}
}- جهاز المكدس (افتراضي): شبكات فضاء المستخدم البحتة عبر gVisor. لا حاجة لجهاز TUN النواة.
- جهاز النظام: ينشئ واجهة TUN حقيقية على نظام التشغيل. يتطلب صلاحيات مرتفعة.
- جهاز مكدس النظام: TUN النظام مع gVisor لمعالجة الحزم.
غلاف جهاز NAT
يغلف NatDevice الجهاز Device لتوفير دعم ICMP/ping عبر إعادة كتابة عنوان المصدر:
type NatDevice interface {
Device
CreateDestination(metadata, routeContext, timeout) (tun.DirectRouteDestination, error)
}
type natDeviceWrapper struct {
Device
ctx context.Context
logger logger.ContextLogger
packetOutbound chan *buf.Buffer
rewriter *ping.SourceRewriter
buffer [][]byte
}إنشاء جهاز NAT
إذا لم يدعم الجهاز الأساسي NAT أصلياً، يتم تطبيق الغلاف:
tunDevice, _ := NewDevice(deviceOptions)
natDevice, isNatDevice := tunDevice.(NatDevice)
if !isNatDevice {
natDevice = NewNATDevice(options.Context, options.Logger, tunDevice)
}اعتراض الحزم
يعترض غلاف NAT القراءات لحقن استجابات ICMP الصادرة ويعترض الكتابات لإعادة كتابة عناوين مصدر ICMP:
func (d *natDeviceWrapper) Read(bufs [][]byte, sizes []int, offset int) (n int, err error) {
select {
case packet := <-d.packetOutbound:
defer packet.Release()
sizes[0] = copy(bufs[0][offset:], packet.Bytes())
return 1, nil
default:
}
return d.Device.Read(bufs, sizes, offset)
}
func (d *natDeviceWrapper) Write(bufs [][]byte, offset int) (int, error) {
for _, buffer := range bufs {
handled, err := d.rewriter.WriteBack(buffer[offset:])
if handled {
// تمت معالجة استجابة ICMP داخلياً
} else {
d.buffer = append(d.buffer, buffer)
}
}
// تحويل الحزم غير ICMP إلى الجهاز الحقيقي
d.Device.Write(d.buffer, offset)
}نقطة النهاية على مستوى النقل
يدير transport/wireguard/endpoint.go دورة حياة جهاز WireGuard:
type Endpoint struct {
options EndpointOptions
peers []peerConfig
ipcConf string
allowedAddress []netip.Prefix
tunDevice Device
natDevice NatDevice
device *device.Device
allowedIPs *device.AllowedIPs
}تكوين IPC
يتم تمرير تكوين WireGuard إلى wireguard-go عبر سلاسل بروتوكول IPC:
privateKeyBytes, _ := base64.StdEncoding.DecodeString(options.PrivateKey)
privateKey := hex.EncodeToString(privateKeyBytes)
ipcConf := "private_key=" + privateKey
if options.ListenPort != 0 {
ipcConf += "\nlisten_port=" + F.ToString(options.ListenPort)
}يتم إنشاء تكوين القرين بالمثل:
func (c peerConfig) GenerateIpcLines() string {
ipcLines := "\npublic_key=" + c.publicKeyHex
if c.endpoint.IsValid() {
ipcLines += "\nendpoint=" + c.endpoint.String()
}
if c.preSharedKeyHex != "" {
ipcLines += "\npreshared_key=" + c.preSharedKeyHex
}
for _, allowedIP := range c.allowedIPs {
ipcLines += "\nallowed_ip=" + allowedIP.String()
}
if c.keepalive > 0 {
ipcLines += "\npersistent_keepalive_interval=" + F.ToString(c.keepalive)
}
return ipcLines
}البدء على مرحلتين
تمتلك نقطة النهاية بدءاً على مرحلتين للتعامل مع حل DNS لنقاط نهاية الأقران:
func (w *Endpoint) Start(stage adapter.StartStage) error {
switch stage {
case adapter.StartStateStart:
return w.endpoint.Start(false) // البدء بدون حل DNS
case adapter.StartStatePostStart:
return w.endpoint.Start(true) // حل نطاقات الأقران الآن
}
}إذا كانت للأقران نقاط نهاية FQDN، يتم تأجيل الحل إلى PostStart عندما يكون DNS متاحاً:
ResolvePeer: func(domain string) (netip.Addr, error) {
endpointAddresses, _ := ep.dnsRouter.Lookup(ctx, domain, outboundDialer.(dialer.ResolveDialer).QueryOptions())
return endpointAddresses[0], nil
},البايتات المحجوزة
يدعم WireGuard بايتات محجوزة لكل قرين (تُستخدم من قبل بعض التنفيذات مثل Cloudflare WARP):
if len(rawPeer.Reserved) > 0 {
if len(rawPeer.Reserved) != 3 {
return nil, E.New("invalid reserved value, required 3 bytes")
}
copy(peer.reserved[:], rawPeer.Reserved[:])
}اختيار الربط
تستخدم نقطة النهاية تنفيذات ربط مختلفة بناءً على نوع المتصل:
wgListener, isWgListener := common.Cast[dialer.WireGuardListener](e.options.Dialer)
if isWgListener {
bind = conn.NewStdNetBind(wgListener.WireGuardControl())
} else {
// ClientBind لاتصالات القرين الواحد
bind = NewClientBind(ctx, logger, dialer, isConnect, connectAddr, reserved)
}نقطة النهاية على مستوى البروتوكول
يعالج protocol/wireguard/endpoint.go تكامل التوجيه:
إعادة كتابة العنوان المحلي
يتم إعادة كتابة الاتصالات إلى عنوان نقطة نهاية WireGuard نفسها إلى عنوان الاسترجاع:
func (w *Endpoint) NewConnectionEx(ctx, conn, source, destination, onClose) {
for _, localPrefix := range w.localAddresses {
if localPrefix.Contains(destination.Addr) {
metadata.OriginDestination = destination
if destination.Addr.Is4() {
destination.Addr = netip.AddrFrom4([4]uint8{127, 0, 0, 1})
} else {
destination.Addr = netip.IPv6Loopback()
}
break
}
}
}حل DNS الصادر
يحل الصادر أسماء FQDN باستخدام موجه DNS:
func (w *Endpoint) DialContext(ctx, network, destination) (net.Conn, error) {
if destination.IsFqdn() {
destinationAddresses, _ := w.dnsRouter.Lookup(ctx, destination.Fqdn, adapter.DNSQueryOptions{})
return N.DialSerial(ctx, w.endpoint, network, destination, destinationAddresses)
}
return w.endpoint.DialContext(ctx, network, destination)
}المسارات المفضلة
تعلن نقطة النهاية عن العناوين التي يمكنها توجيهها، مما يمكّن الموجه من اختيارها للوجهات المطابقة:
func (w *Endpoint) PreferredAddress(address netip.Addr) bool {
return w.endpoint.Lookup(address) != nil
}تكامل مدير الإيقاف المؤقت
تستجيب نقطة النهاية لأحداث إيقاف/إيقاظ الجهاز (مثل نوم الهاتف المحمول):
func (e *Endpoint) onPauseUpdated(event int) {
switch event {
case pause.EventDevicePaused, pause.EventNetworkPause:
e.device.Down()
case pause.EventDeviceWake, pause.EventNetworkWake:
e.device.Up()
}
}مثال على التكوين
{
"type": "wireguard",
"tag": "wg-ep",
"system": false,
"name": "wg0",
"mtu": 1420,
"address": ["10.0.0.2/32", "fd00::2/128"],
"private_key": "base64-encoded-private-key",
"peers": [
{
"address": "server.example.com",
"port": 51820,
"public_key": "base64-encoded-public-key",
"pre_shared_key": "optional-base64-psk",
"allowed_ips": ["0.0.0.0/0", "::/0"],
"persistent_keepalive_interval": 25,
"reserved": [0, 0, 0]
}
],
"workers": 2
}