From a Local Proxy to a System-Wide iOS Tunnel

OpenFlux’s iOS integration shows that an entry boundary can change without replacing the carrier or the exit node. The local-proxy mode gives one app a SOCKS5 endpoint; the Network Extension mode captures device IPv4 traffic and sends selected packets through the Go core. This chapter follows the C/Swift lifecycle, the DNS-over-TLS choices, and the system packet path so the reader can see both what the mobile design adds and what it deliberately leaves unsupported.

The Go core has to speak Swift’s lifecycle language

The iOS app does not call the Go program’s command-line flags directly. The Go core is built as a static library and exposes a C-compatible bridge. Swift calls functions to start and stop the client, query running and connected state, fetch a compact statistics object, drain buffered logs, and free returned strings. This bridge is a second boundary around the transport and tunnel: Swift owns app and extension lifecycle, while Go owns the network state that continues after a call returns.

That distinction changes how startup failures are handled. The bridge checks whether the client is already running, probes the requested local SOCKS5 address before starting the transport, chooses the selected carrier, and binds the listener. It converts common failures—already running, unsupported transport, transport startup failure, address conflict, or recovered panic—into return codes and log messages rather than letting an error surface only in a background goroutine. The UI can therefore report that a local port is occupied before a user tries to interpret a later connection timeout.

The bridge also serializes shared state. A mutex protects the running flag and pointers to the transport and SOCKS5 server. Stop closes the listener and transport and clears those references. A ring buffer holds recent log lines so Swift can drain them without asking the Go library to expose an unbounded stream. Verbose packet logging is off by default because the code treats per-packet output as expensive. These decisions are not cosmetic; they define what the mobile app can observe and when it is safe to start or stop the underlying service.

The local-proxy mode remains conceptually close to the desktop path. The SwiftUI app collects a document URL or MAX credentials, starts the Go client on a local address, and lets a selected application use the SOCKS5 proxy. A test button can exercise traffic through that proxy, while the UI separately displays connected state, counters, and log output. The fact that the app is written in Swift does not make the transport a native iOS protocol. The same Go tunnel and carrier boundaries remain underneath the bridge.

Name resolution receives special treatment on iOS. The bridge installs a Go resolver that uses DNS-over-TLS connections to configured resolvers instead of relying entirely on the device’s ordinary resolver. The reason is specific: a local DNS answer can be incorrect or poisoned for a destination, and the wrong address would be handed to the exit node before the carrier saw any packet. Encrypting the resolver connection and validating its TLS endpoint can improve the integrity of that lookup path.

That improvement has a defined scope.