واجهة المنصة
توفر واجهة المنصة طبقة تجريد للمنصات المحمولة (Android/iOS) لدمج sing-box في التطبيقات الأصلية عبر ربطات gomobile. تتعامل مع إدارة جهاز TUN، ومراقبة الشبكة، وتحديد العمليات، والعمليات على مستوى النظام.
المصدر: experimental/libbox/، adapter/platform.go
هندسة ذات طبقتين
يوجد نوعان من PlatformInterface:
adapter.PlatformInterface(داخلي) -- الواجهة المستخدمة داخل نواة sing-boxlibbox.PlatformInterface(خارجي) -- الواجهة المتوافقة مع gomobile التي ينفذها التطبيق المضيف
platformInterfaceWrapper في libbox يجسر بينهما:
var _ adapter.PlatformInterface = (*platformInterfaceWrapper)(nil)
type platformInterfaceWrapper struct {
iif PlatformInterface // واجهة gomobile من التطبيق المضيف
useProcFS bool
networkManager adapter.NetworkManager
myTunName string
defaultInterfaceAccess sync.Mutex
defaultInterface *control.Interface
isExpensive bool
isConstrained bool
}adapter.PlatformInterface (داخلي)
type PlatformInterface interface {
Initialize(networkManager NetworkManager) error
UsePlatformAutoDetectInterfaceControl() bool
AutoDetectInterfaceControl(fd int) error
UsePlatformInterface() bool
OpenInterface(options *tun.Options, platformOptions TunPlatformOptions) (tun.Tun, error)
UsePlatformDefaultInterfaceMonitor() bool
CreateDefaultInterfaceMonitor(logger logger.Logger) tun.DefaultInterfaceMonitor
UsePlatformNetworkInterfaces() bool
NetworkInterfaces() ([]NetworkInterface, error)
UnderNetworkExtension() bool
NetworkExtensionIncludeAllNetworks() bool
ClearDNSCache()
RequestPermissionForWIFIState() error
ReadWIFIState() WIFIState
SystemCertificates() []string
UsePlatformConnectionOwnerFinder() bool
FindConnectionOwner(request *FindConnectionOwnerRequest) (*ConnectionOwner, error)
UsePlatformWIFIMonitor() bool
UsePlatformNotification() bool
SendNotification(notification *Notification) error
UsePlatformNeighborResolver() bool
StartNeighborMonitor(listener NeighborUpdateListener) error
CloseNeighborMonitor(listener NeighborUpdateListener) error
}كل طريقة UsePlatform*() تُرجع true للإشارة إلى أن المنصة توفر تلك القدرة، مما يجعل sing-box يستخدم تطبيق المنصة بدلاً من تطبيق Go الافتراضي.
libbox.PlatformInterface (خارجي/gomobile)
type PlatformInterface interface {
LocalDNSTransport() LocalDNSTransport
UsePlatformAutoDetectInterfaceControl() bool
AutoDetectInterfaceControl(fd int32) error
OpenTun(options TunOptions) (int32, error) // يُرجع واصف ملف
UseProcFS() bool
FindConnectionOwner(ipProtocol int32, sourceAddress string,
sourcePort int32, destinationAddress string,
destinationPort int32) (*ConnectionOwner, error)
StartDefaultInterfaceMonitor(listener InterfaceUpdateListener) error
CloseDefaultInterfaceMonitor(listener InterfaceUpdateListener) error
GetInterfaces() (NetworkInterfaceIterator, error)
UnderNetworkExtension() bool
IncludeAllNetworks() bool
ReadWIFIState() *WIFIState
SystemCertificates() StringIterator
ClearDNSCache()
SendNotification(notification *Notification) error
StartNeighborMonitor(listener NeighborUpdateListener) error
CloseNeighborMonitor(listener NeighborUpdateListener) error
RegisterMyInterface(name string)
}الاختلافات الرئيسية عن الواجهة الداخلية:
- تستخدم
int32بدلاً منint(توافق gomobile) - تُرجع مكررات بدلاً من شرائح (gomobile لا يدعم شرائح Go)
OpenTunيُرجع واصف ملف خام بدلاً من كائنtun.TunStringIteratorيغلف[]stringلاستهلاك gomobile
إدارة جهاز TUN
فتح TUN
يحوّل غلاف المنصة بين أنواع TUN في libbox والأنواع الداخلية:
func (w *platformInterfaceWrapper) OpenInterface(options *tun.Options, platformOptions) (tun.Tun, error) {
// 1. بناء نطاقات التوجيه التلقائي
routeRanges, _ := options.BuildAutoRouteRanges(true)
// 2. استدعاء المنصة لفتح TUN (يُرجع fd)
tunFd, _ := w.iif.OpenTun(&tunOptions{options, routeRanges, platformOptions})
// 3. الحصول على اسم النفق من fd
options.Name, _ = getTunnelName(tunFd)
// 4. التسجيل مع مراقب الواجهة
options.InterfaceMonitor.RegisterMyInterface(options.Name)
// 5. نسخ fd (قد تغلق المنصة الأصلي)
dupFd, _ := dup(int(tunFd))
options.FileDescriptor = dupFd
// 6. إنشاء tun.Tun من الخيارات
return tun.New(*options)
}دالة getTunnelName خاصة بالمنصة:
- Darwin: تقرأ اسم الواجهة من fd عبر
ioctl - Linux: تقرأ من الرابط الرمزي
/proc/self/fd/<fd>وتستخرج اسم tun - أخرى: تُرجع اسماً بديلاً
مراقب الواجهة الافتراضية
مراقب الواجهة الافتراضية للمنصة يغلف عمليات استدعاء تغيير الشبكة من التطبيق المضيف:
type platformDefaultInterfaceMonitor struct {
*platformInterfaceWrapper
logger logger.Logger
callbacks list.List[tun.DefaultInterfaceUpdateCallback]
myInterface string
}تدفق التحديث
عندما يكتشف التطبيق المضيف تغييراً في الشبكة:
func (m *platformDefaultInterfaceMonitor) UpdateDefaultInterface(
interfaceName string, interfaceIndex32 int32,
isExpensive bool, isConstrained bool) {
// 1. تحديث علامات التكلفة/القيود
// 2. إخبار مدير الشبكة بتحديث الواجهات
// 3. البحث عن الواجهة الجديدة بالفهرس
// 4. تحديث الواجهة الافتراضية المخزنة
// 5. إخطار جميع عمليات الاستدعاء المسجلة (إذا تغيرت الواجهة)
}إذا كان interfaceIndex32 == -1، فإن الجهاز لا يملك اتصالاً بالشبكة (جميع عمليات الاستدعاء تتلقى nil).
على Android، قد يتم إرسال التحديث إلى goroutine جديد عبر sFixAndroidStack لتجنب خلل في وقت تشغيل Go يتعلق بأحجام مكدسات خيوط Android.
تعداد واجهات الشبكة
func (w *platformInterfaceWrapper) NetworkInterfaces() ([]adapter.NetworkInterface, error) {
interfaceIterator, _ := w.iif.GetInterfaces()
var interfaces []adapter.NetworkInterface
for _, netInterface := range iteratorToArray(interfaceIterator) {
// تخطي واجهة TUN الخاصة بنا
if netInterface.Name == w.myTunName {
continue
}
interfaces = append(interfaces, adapter.NetworkInterface{
Interface: control.Interface{
Index: int(netInterface.Index),
MTU: int(netInterface.MTU),
Name: netInterface.Name,
Addresses: common.Map(iteratorToArray(netInterface.Addresses), netip.MustParsePrefix),
Flags: linkFlags(uint32(netInterface.Flags)),
},
Type: C.InterfaceType(netInterface.Type),
DNSServers: iteratorToArray(netInterface.DNSServer),
Expensive: netInterface.Metered || isDefault && w.isExpensive,
Constrained: isDefault && w.isConstrained,
})
}
// إزالة التكرار بالاسم
return common.UniqBy(interfaces, func(it) string { return it.Name }), nil
}أنواع الواجهات هي:
const (
InterfaceTypeWIFI = int32(C.InterfaceTypeWIFI)
InterfaceTypeCellular = int32(C.InterfaceTypeCellular)
InterfaceTypeEthernet = int32(C.InterfaceTypeEthernet)
InterfaceTypeOther = int32(C.InterfaceTypeOther)
)مالك اتصال العملية
يدعم غلاف المنصة وضعين للعثور على مالكي الاتصالات:
func (w *platformInterfaceWrapper) FindConnectionOwner(request) (*ConnectionOwner, error) {
if w.useProcFS {
// الوضع 1: فحص procfs المباشر (Android مع صلاحيات root/VPN)
uid := procfs.ResolveSocketByProcSearch(network, source, destination)
return &ConnectionOwner{UserId: uid}, nil
}
// الوضع 2: تفويض للمنصة (يستخدم ConnectivityManager في Android)
result, _ := w.iif.FindConnectionOwner(...)
return &ConnectionOwner{
UserId: result.UserId,
ProcessPath: result.ProcessPath,
AndroidPackageName: result.AndroidPackageName,
}, nil
}الإعداد والتهيئة
دالة Setup() تهيئ المسارات والخيارات العامة للمنصات المحمولة:
type SetupOptions struct {
BasePath string // دليل بيانات التطبيق
WorkingPath string // دليل العمل لملفات التهيئة
TempPath string // الملفات المؤقتة
FixAndroidStack bool // حل بديل لخلل وقت تشغيل Go
CommandServerListenPort int32 // منفذ خادم الأوامر المحلي
CommandServerSecret string // سر المصادقة
LogMaxLines int // حجم مخزن السجل
Debug bool // تفعيل ميزات التصحيح
}حالة وكيل النظام
type SystemProxyStatus struct {
Available bool
Enabled bool
}يمثل هذا النوع ما إذا كانت تهيئة وكيل النظام متاحة على المنصة وما إذا كانت مفعلة حالياً.
امتداد شبكة iOS
علامتان مهمتان لامتداد شبكة iOS (NEPacketTunnelProvider):
UnderNetworkExtension(): تُرجع true عند التشغيل داخل عملية امتداد شبكة iOS، والتي لها قيود مختلفة في الذاكرة والقدراتNetworkExtensionIncludeAllNetworks(): تُرجع true عندما يكون استحقاقincludeAllNetworksنشطاً، مما يوجه كل حركة مرور الجهاز (بما في ذلك عمليات النظام) عبر النفق
الإشعارات
type Notification struct {
Identifier string
TypeName string
TypeID int32
Title string
Subtitle string
Body string
OpenURL string
}تُستخدم الإشعارات للتنبيهات على مستوى النظام (مثل فشل تحديث مجموعة القواعد، تحذيرات انتهاء صلاحية الشهادة).
قواعد عند الطلب (iOS)
type OnDemandRule interface {
Target() int32
DNSSearchDomainMatch() StringIterator
DNSServerAddressMatch() StringIterator
InterfaceTypeMatch() int32
SSIDMatch() StringIterator
ProbeURL() string
}تتحكم هذه القواعد في متى يجب تنشيط نفق VPN على iOS، بناءً على ظروف الشبكة (SSID، نوع الواجهة، تهيئة DNS).
ملاحظات إعادة التنفيذ
- قيود gomobile: واجهة libbox تستخدم
int32بدلاً منint، ومكررات بدلاً من شرائح، وأنواع مؤشرات بدلاً من أنواع قيم. هذه كلها قيود gomobile - نسخ واصف الملف: يجب نسخ fd الخاص بـ TUN باستخدام
dup()لأن المنصة قد تغلق fd الأصلي بعد إرجاعه - تصفية الواجهات: يجب استبعاد واجهة TUN نفسها من قائمة واجهات الشبكة لمنع حلقات التوجيه
- إصلاح مكدس Android: علم
sFixAndroidStackيرسل تحديثات الواجهة إلى goroutines جديدة لتجنب مشكلة Go رقم #68760 المتعلقة بأحجام مكدسات خيوط Android - الاتصال ثنائي الاتجاه: واجهة المنصة ثنائية الاتجاه -- التطبيق المضيف يستدعي sing-box (عبر
BoxService) و sing-box يستدعي التطبيق المضيف (عبرPlatformInterface) - خادم الأوامر: خادم TCP محلي منفصل (غير موضح هنا) يتعامل مع IPC بين واجهة التطبيق المضيف وخدمة sing-box العاملة في الخلفية