Platform Interface
The platform interface provides an abstraction layer for mobile platforms (Android/iOS) to integrate sing-box into native apps via gomobile bindings. It handles TUN device management, network monitoring, process identification, and system-level operations.
Source: experimental/libbox/, adapter/platform.go
Two-Layer Architecture
There are two PlatformInterface types:
adapter.PlatformInterface(internal) -- the interface used within sing-box's corelibbox.PlatformInterface(external) -- the gomobile-compatible interface implemented by the host app
The platformInterfaceWrapper in libbox bridges between them:
var _ adapter.PlatformInterface = (*platformInterfaceWrapper)(nil)
type platformInterfaceWrapper struct {
iif PlatformInterface // gomobile interface from host app
useProcFS bool
networkManager adapter.NetworkManager
myTunName string
defaultInterfaceAccess sync.Mutex
defaultInterface *control.Interface
isExpensive bool
isConstrained bool
}adapter.PlatformInterface (Internal)
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
}Each UsePlatform*() method returns true to indicate the platform provides that capability, causing sing-box to use the platform implementation instead of the default Go implementation.
libbox.PlatformInterface (External/gomobile)
type PlatformInterface interface {
LocalDNSTransport() LocalDNSTransport
UsePlatformAutoDetectInterfaceControl() bool
AutoDetectInterfaceControl(fd int32) error
OpenTun(options TunOptions) (int32, error) // returns file descriptor
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)
}Key differences from the internal interface:
- Uses
int32instead ofint(gomobile compatibility) - Returns iterators instead of slices (gomobile does not support Go slices)
OpenTunreturns a raw file descriptor instead of atun.TunobjectStringIteratorwraps[]stringfor gomobile consumption
TUN Device Management
Opening the TUN
The platform wrapper converts between libbox and internal TUN types:
func (w *platformInterfaceWrapper) OpenInterface(options *tun.Options, platformOptions) (tun.Tun, error) {
// 1. Build auto-route ranges
routeRanges, _ := options.BuildAutoRouteRanges(true)
// 2. Call platform to open TUN (returns fd)
tunFd, _ := w.iif.OpenTun(&tunOptions{options, routeRanges, platformOptions})
// 3. Get tunnel name from fd
options.Name, _ = getTunnelName(tunFd)
// 4. Register with interface monitor
options.InterfaceMonitor.RegisterMyInterface(options.Name)
// 5. Dup the fd (platform may close original)
dupFd, _ := dup(int(tunFd))
options.FileDescriptor = dupFd
// 6. Create tun.Tun from options
return tun.New(*options)
}The getTunnelName function is platform-specific:
- Darwin: reads the interface name from the fd via
ioctl - Linux: reads from
/proc/self/fd/<fd>symlink and extracts the tun name - Other: returns a placeholder name
Default Interface Monitor
The platform default interface monitor wraps the host app's network change callbacks:
type platformDefaultInterfaceMonitor struct {
*platformInterfaceWrapper
logger logger.Logger
callbacks list.List[tun.DefaultInterfaceUpdateCallback]
myInterface string
}Update Flow
When the host app detects a network change:
func (m *platformDefaultInterfaceMonitor) UpdateDefaultInterface(
interfaceName string, interfaceIndex32 int32,
isExpensive bool, isConstrained bool) {
// 1. Update expense/constrained flags
// 2. Tell network manager to refresh interfaces
// 3. Look up the new interface by index
// 4. Update stored default interface
// 5. Notify all registered callbacks (if interface changed)
}If interfaceIndex32 == -1, the device has no network connectivity (all callbacks receive nil).
On Android, the update may be dispatched to a new goroutine via sFixAndroidStack to work around a Go runtime bug with Android thread stacks.
Network Interface Enumeration
func (w *platformInterfaceWrapper) NetworkInterfaces() ([]adapter.NetworkInterface, error) {
interfaceIterator, _ := w.iif.GetInterfaces()
var interfaces []adapter.NetworkInterface
for _, netInterface := range iteratorToArray(interfaceIterator) {
// Skip our own TUN interface
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,
})
}
// Deduplicate by name
return common.UniqBy(interfaces, func(it) string { return it.Name }), nil
}Interface types are:
const (
InterfaceTypeWIFI = int32(C.InterfaceTypeWIFI)
InterfaceTypeCellular = int32(C.InterfaceTypeCellular)
InterfaceTypeEthernet = int32(C.InterfaceTypeEthernet)
InterfaceTypeOther = int32(C.InterfaceTypeOther)
)Process Connection Owner
The platform wrapper supports two modes for finding connection owners:
func (w *platformInterfaceWrapper) FindConnectionOwner(request) (*ConnectionOwner, error) {
if w.useProcFS {
// Mode 1: Direct procfs scanning (Android with root/VPN)
uid := procfs.ResolveSocketByProcSearch(network, source, destination)
return &ConnectionOwner{UserId: uid}, nil
}
// Mode 2: Delegate to platform (uses Android's ConnectivityManager)
result, _ := w.iif.FindConnectionOwner(...)
return &ConnectionOwner{
UserId: result.UserId,
ProcessPath: result.ProcessPath,
AndroidPackageName: result.AndroidPackageName,
}, nil
}Setup and Initialization
The Setup() function configures global paths and options for mobile platforms:
type SetupOptions struct {
BasePath string // app data directory
WorkingPath string // working directory for config files
TempPath string // temporary files
FixAndroidStack bool // workaround for Go runtime bug
CommandServerListenPort int32 // local command server port
CommandServerSecret string // authentication secret
LogMaxLines int // log buffer size
Debug bool // enable debug features
}System Proxy Status
type SystemProxyStatus struct {
Available bool
Enabled bool
}This type represents whether system proxy configuration is available on the platform and whether it is currently enabled.
iOS Network Extension
Two important flags for iOS Network Extension (NEPacketTunnelProvider):
UnderNetworkExtension(): Returns true when running inside an iOS Network Extension process, which has different memory and capability constraintsNetworkExtensionIncludeAllNetworks(): Returns true when theincludeAllNetworksentitlement is active, which routes all device traffic (including system processes) through the tunnel
Notifications
type Notification struct {
Identifier string
TypeName string
TypeID int32
Title string
Subtitle string
Body string
OpenURL string
}Notifications are used for system-level alerts (e.g., rule set update failures, certificate expiration warnings).
On-Demand Rules (iOS)
type OnDemandRule interface {
Target() int32
DNSSearchDomainMatch() StringIterator
DNSServerAddressMatch() StringIterator
InterfaceTypeMatch() int32
SSIDMatch() StringIterator
ProbeURL() string
}These rules control when the VPN tunnel should be activated on iOS, based on network conditions (SSID, interface type, DNS configuration).
Reimplementation Notes
- gomobile constraints: The libbox interface uses
int32instead ofint, iterators instead of slices, and pointer types instead of value types. These are all gomobile limitations - File descriptor duplication: The TUN fd must be
dup()-ed because the platform may close the original fd after returning it - Interface filtering: The TUN interface itself must be excluded from the list of network interfaces to prevent routing loops
- Android stack fix: The
sFixAndroidStackflag dispatches interface updates to new goroutines to work around Go issue #68760 related to Android thread stack sizes - Bidirectional communication: The platform interface is bidirectional -- the host app calls sing-box (via
BoxService) and sing-box calls back to the host app (viaPlatformInterface) - Command server: A separate local TCP server (not shown here) handles IPC between the host app UI and the sing-box service running in the background