An iOS app for reaching private resources — hosts on your server's network
by hostname (including names that only resolve via the server's DNS),
the server's own localhost, or hosts by IP — without a VPN. It's the iOS
client for flextunnel, a SOCKS5-over-QUIC proxy where the
server makes the outbound TCP connection from its own network, resolving DNS
server-side when the target is a hostname. Because the transport is iroh
QUIC (NAT traversal, relay fallback, TLS 1.3), the app dials the server by its
endpoint id — the server needs no public inbound port, and neither end needs
root or a TUN device.
There are two ways to use the tunnel, chosen on the setup screen:
- Browse the web — a built-in browser routed through the tunnel. This is a
private-network access browser, not a privacy/anonymity browser: it
behaves like a mainstream browser and split-tunnels by default. The flextunnel
Rust core is embedded on-device (via
libflextunnel.xcframework) and runs a local SOCKS5 listener on loopback. When the tunnel set is hostname-only, the SwiftUIWebViewscopes that listener to it withProxyConfiguration.matchDomains(WKWebsiteDataStore.proxyConfigurations), so only on-list hosts go through the proxy and everything else connects directly. When the set is full-tunnel (*/ default-route CIDR) or routes any CIDR — patternsmatchDomainscan't express — host scoping is off and WebKit routes every host through the proxy, so the direct-connection and proxy-health-independence behavior doesn't apply. - Forward ports — run the proxy without the browser and forward local
localhostports to private hosts, so other apps on the device (SSH, RDP, databases…) can reach them.
- Split-tunnel routing — the two layers that
decide tunnel-vs-direct (the Rust core and WebKit
matchDomains), the server-pushed tunnel set, browser independence from proxy health, and verifying the split. - Proxy-only mode & port forwarding — the SOCKS5 endpoint and localhost port forwards for other apps, background behavior, what forwards are good for, and troubleshooting.
- Background keep-alive — the opt-in location session that keeps port forwards alive while backgrounded, where the decision is made, its inactivity limit, and the ~30 s fallback without it. Browser mode deliberately uses normal iOS/WebKit background suspension.
- Local FFI development — building against a
local
../flextunnelRust checkout instead of the pinned release. - Running CI locally —
ci/ci.shruns the workflows' checks against the working tree here (Simulator build, a launch smoke test, the unsigned-archive assertions, the signed device slice);scripts/run-device-ios-host.shships that signed build to the Mac the iPhone is attached to, then installs and launches it on the phone from there. - Building in a macOS VM — why a UTM/Tart guest can never install onto a physical device, and the build-in-the-VM / install-from-the-host split that works instead.
- Sign an unsigned IPA — verify a prerelease, apply team-owned bundle IDs, sign the widget and app with separate profiles, and preserve the background location configuration.
- Xcode (tested with 26.x) on Apple Silicon.
xcodegen:brew install xcodegen.- Rust with the iOS target:
rustup target add aarch64-apple-ios.
The Rust static library (libflextunnel.xcframework) is delivered via a local
Swift package, Packages/Flextunnel. Its binary target downloads the pinned
release zip by URL + checksum, so a clean checkout builds reproducibly — there's
no vendored copy to stage. To move to a new release, run
scripts/bump-xcframework.sh <tag> (rewrites the url + checksum).
-
Generate the Xcode project (Xcode resolves and downloads the package on first build):
xcodegen generate open Flextunnel.xcodeproj
-
Set signing. Copy the sample and fill in your Team ID (gitignored, so it stays local and survives
xcodegen generate):cp Developer.local.xcconfig.sample Developer.local.xcconfig # then edit Developer.local.xcconfig and set DEVELOPMENT_TEAMThe committed
Developer.xcconfig(wired viaconfigFilesinproject.yml)#include?s it, so Xcode andxcodebuildsign automatically. You can still just pick a Team on theFlextunnelApptarget in Xcode instead. -
Run on a device or the Simulator. Enter:
- Profile — which saved connection the fields below belong to. Manage… to add, rename or delete one. Each profile keeps its own server, auth key, relays and port forwards, so pointing the app at another server is a pick rather than a retype — but only the selected profile connects, one session at a time (unlike the desktop, where every profile can be up at once).
- Server node id — the flextunnel server's iroh endpoint id.
- Auth key — pick from a named key list (Manage… to generate one on the
device, paste an existing secret key, or export one); the secrets stay in
the Keychain, and the shown public key goes on the server's
authorized_keys_file. The list is shared across profiles — the same keypair can authenticate several of them. - Relay URLs — optional hints; leave blank for iroh defaults.
Pick Browse the web or Forward ports, then tap the start button. Tunnel health, the bound port, and the active split-tunnel set are shown under Tunnel status in the browser, or on the port-forwarding screen.
scripts/create-archive-ios.sh builds a Release .xcarchive and exports a
signed .ipa in one step. It needs an Apple Developer Team ID, which it reads
from Developer.local.xcconfig (gitignored, per-developer) or a --team-id flag.
-
Set your Team ID (once):
cp Developer.local.xcconfig.sample Developer.local.xcconfig # then edit Developer.local.xcconfig and fill in DEVELOPMENT_TEAM -
Generate the project if you haven't already (
xcodegen generate). -
Build and export:
scripts/create-archive-ios.sh
The archive lands in
build/flextunnel-ios.xcarchiveand the.ipainbuild/export/. Pass--team-id <ID>to override the xcconfig, or--allow-provisioning-updatesto let Xcode create/update signing assets. Seescripts/create-archive-ios.sh --helpfor all options (configuration, output paths, and export method, which defaults todebugging).
Run the Build unsigned iOS artifacts workflow manually in GitHub Actions.
It builds without a development team, uploads the unsigned IPA, unsigned
.xcarchive, and checksums as a workflow artifact, and publishes the same files
in a prerelease named yyyymmddhhmmss-<short-git-hash>.
The IPA cannot be installed as downloaded. Each user must sign both the main app and its WidgetKit extension with separate profiles owned by their Apple developer team. See Sign the unsigned iOS IPA for the complete process. The workflow also verifies that the unsigned build still contains the app's background location mode and usage description.
- Targets iOS 26: the browser uses the SwiftUI
WebView/WebPageAPI. (WKWebsiteDataStore.proxyConfigurations, the runtime proxy hook, requires iOS 17+.) libflextunnel.xcframeworkis arm64-only; build/verify against a pinned arm64 iOS 26 Simulator, e.g.-destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2'.- The
.xcodeprojis git-ignored on purpose — regenerate it withxcodegen. The xcframework is fetched by Swift Package Manager (pinned release, or a local build in FFI-dev mode), not vendored into this repo.