Интерфейс платформы
Интерфейс платформы предоставляет уровень абстракции для мобильных платформ (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 обновление может быть диспетчеризировано в новую горутину через 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 Network Extension (NEPacketTunnelProvider):
UnderNetworkExtension(): Возвращает true при работе внутри процесса iOS Network Extension, который имеет другие ограничения по памяти и возможностям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 - Дублирование файлового дескриптора: TUN fd необходимо продублировать через
dup(), потому что платформа может закрыть оригинальный fd после его возврата - Фильтрация интерфейсов: Сам TUN-интерфейс должен быть исключён из списка сетевых интерфейсов для предотвращения петель маршрутизации
- Исправление стека Android: Флаг
sFixAndroidStackдиспетчеризирует обновления интерфейсов в новые горутины для обхода Go issue #68760, связанного с размерами стеков потоков Android - Двунаправленная коммуникация: Интерфейс платформы двунаправленный — хост-приложение вызывает sing-box (через
BoxService), и sing-box вызывает обратно хост-приложение (черезPlatformInterface) - Командный сервер: Отдельный локальный TCP-сервер (здесь не показан) обеспечивает IPC между UI хост-приложения и сервисом sing-box, работающим в фоне