A SOCKS5/HTTP-proxy, and port-forward-over-QUIC split tunnel. The client runs optional local SOCKS5 and HTTP proxy listeners, plus optional port forwards that send a local port straight to one server-side address. Each proxy request is matched against the server-pushed tunnel set: routed targets are tunneled as reliable QUIC bi-streams to the server, which performs DNS resolution and the outbound TCP connection from its own network, then pipes bytes back; off-list targets are connected directly from the client device. A forwarded port does no matching on the client: everything it accepts goes to the server, which enforces its routed set there.
This lets you reach hosts that are only reachable from the server side — a
private network, the server's own localhost, or names that only resolve via
the server's DNS — without a VPN. Because it uses ordinary userspace sockets
(no TUN device), neither the client nor the server needs admin/root.
Transport, NAT traversal, relay fallback, and TLS 1.3 encryption are provided by
iroh: the client dials the server by its
EndpointId, so the server needs no public inbound port or port forwarding.
local app ──SOCKS5/HTTP──► flextunnel client (optional listeners, e.g. 127.0.0.1:1080 / :8081)
│ one iroh QUIC connection (fixed ALPN + auth handshake)
│ ├─ control stream: Hello / HelloResponse
│ └─ N data streams: [target header][reply][raw bytes]
▼
flextunnel server (no root, no TUN)
│ per stream: resolve DNS ─► TcpStream::connect
▼
target host:port (reachable from the SERVER's network)
- SOCKS5
CONNECTonly. No UDPASSOCIATE, noBIND. The optional HTTP proxy supports HTTPCONNECTplus absolute-URI plain-HTTP forwarding. - The local SOCKS5 and HTTP proxy listeners are unauthenticated, and each is
disabled unless explicitly configured. Any local user or process that can reach
them can use the tunnel, so they bind
127.0.0.1only (you configure just the port, like the desktop client) and flextunnel assumes a trusted, single-user host. The client keypair authenticates the client process to the server; it does not authenticate local callers of the proxy front-ends, and is no substitute for OS-level access control on a shared machine.
flextunnel lets a set of trusted clients reach resources on the server's
side of the network. Both ends are run by the same trusted party: whoever runs
the server decides which client keys it authorizes. It is not a multi-tenant
service and does not defend the server against the clients it admits — a client
with an authorized key can, by design, reach whatever the server's network can
reach (including the server's own localhost). Authorize keys accordingly, and
scope the server's network access if that reach is too broad. The threats it does
address are on-path attackers (encryption + per-client keypairs) and accidental
misconfiguration (e.g. duplicate-id detection catching two clients or servers
started with the same identity — an operator guard rail, not an adversary
defense).
A per-client ed25519 keypair gates every connection:
- Client keypair — each client generates a keypair in the shared
flexaccess-keys format
(
flexaccess-keys generate-auth-key); the server keeps the public keys in an ssh-style authorized-keys file. In the handshake the client sends its public key, its (ephemeral) iroh endpoint id, and a signature over that id; the server accepts only if the claimed id matches the connection's TLS-authenticated id, the signature verifies, and the key is authorized — the secret never leaves the client, and a captured handshake cannot be replayed from another endpoint.
(The keyless exceptions are server-to-server bridges and --quick sessions,
whose credential is the peer's TLS-authenticated EndpointId, checked
natively against an allowlist at the handshake — the authorized_keys model.)
The QUIC ALPN is a fixed protocol identifier (flextunnel/1), not a secret: it
ensures both peers speak the flextunnel protocol but provides no access control
on its own.
All payload is end-to-end encrypted by QUIC/TLS 1.3.
Prebuilt release assets are published on the
GitHub Releases page.
Stable releases include flextunnel for Linux
amd64/arm64, macOS arm64, and Windows amd64, plus the iOS xcframework asset.
Automated prereleases currently include Linux amd64/arm64, macOS arm64, and the
iOS xcframework, but skip Windows. The install scripts download the latest
binary and verify its SHA-256 checksum. On Linux/macOS this installs to a
per-user location (~/.local/bin, no root required). On Windows this installs
system-wide to C:\Program Files\flextunnel and updates the machine PATH,
which requires an elevated (Administrator) PowerShell session — running
the installed binary afterward does not.
flextunnel (server / client) — Linux / macOS:
curl -sSL https://flexaccessdev.github.io/flextunnel/install.sh | bashflextunnel (server / client) — Windows (PowerShell):
irm https://flexaccessdev.github.io/flextunnel/install.ps1 | iexOptions: append -s -- --prerelease (bash) for the latest prerelease, a release
tag to pin a version, or --download-only / -DownloadOnly to fetch the binary
without installing. -PreRelease is also accepted by the Windows installer when
the selected prerelease includes a Windows asset. A container image is also
published to ghcr.io/flexaccessdev/flextunnel.
Stable releases also include the desktop client
(flextunnel-desktop-macos-arm64.dmg and
flextunnel-desktop-windows-amd64.msi). The installers are unsigned — if
you'd rather not apply the workarounds below, build it yourself instead:
cargo build --release -p flextunnel-desktop
# binary: target/release/flextunnel-desktop — locally built binaries are not
# quarantined, so no workaround is neededThat produces the bare executable, not a .app bundle — so on macOS it has
no Info.plist and will show a Dock icon instead of running as a pure menu-bar
app. For the proper bundle, build it the way CI does with cargo-packager
(macOS only; app gives flextunnel.app, dmg gives the drag-to-Applications
disk image):
cargo install cargo-packager # or: cargo binstall cargo-packager
cargo packager --release -p flextunnel-desktop --formats app
# bundle: target/release/flextunnel.app (locally built → not quarantined,
# so no Gatekeeper workaround needed; use --formats dmg for a .dmg instead)Otherwise:
macOS: because the app is not notarized, Gatekeeper quarantines the download and shows "flextunnel" is damaged and can't be opened.
The cleanest fix is to avoid the quarantine flag in the first place: browsers
set com.apple.quarantine on downloads, but command-line tools like curl and
wget do not. Download the disk image from the terminal instead:
# Replace vX.Y.Z with the release tag from the Releases page.
curl -fL -o flextunnel-desktop.dmg \
https://github.com/flexaccessdev/flextunnel/releases/download/vX.Y.Z/flextunnel-desktop-macos-arm64.dmgThen in Finder: double-click the .dmg to open it, drag flextunnel.app onto
the Applications shortcut in the window (choose Replace if an older copy
is already there), then eject the mounted image.
macOS zsh note: the shell snippets in this README include
#comment lines. zsh (the macOS default shell) treats#as a comment on an interactive prompt only aftersetopt interactivecomments; without it, pasting a#line reportscommand not found. Runsetopt interactivecommentsonce per session, or just omit the comment lines when pasting.
If you already downloaded the .dmg via a browser, the quarantine flag
propagates to the app you copy out of it; remove it after installing (into
/Applications) instead:
xattr -cr /Applications/flextunnel.appAlternatively, right-click the app and choose Open the first time.
Windows: SmartScreen's "Windows protected your PC" warning is triggered by
the Mark of the Web (the Zone.Identifier stream), which — like macOS
quarantine — is set by browsers but not by command-line tools. Avoid it the same
way: download with curl.exe (bundled in Windows 10+) or PowerShell instead of a
browser. Replace vX.Y.Z with the release tag from the Releases page:
curl.exe -fL -o flextunnel-desktop.msi `
https://github.com/flexaccessdev/flextunnel/releases/download/vX.Y.Z/flextunnel-desktop-windows-amd64.msi
# or: Invoke-WebRequest -OutFile flextunnel-desktop.msi `
# https://github.com/flexaccessdev/flextunnel/releases/download/vX.Y.Z/flextunnel-desktop-windows-amd64.msiIf you already downloaded via a browser, strip the mark instead:
Unblock-File .\flextunnel-desktop-windows-amd64.msi (or right-click →
Properties → Unblock). Otherwise click More info → Run anyway.
cargo build --release
# binary: target/release/flextunnelRequires a recent Rust toolchain (edition 2024). A bare cargo build --release
uses the workspace's default members and builds the CLI, but not
the iOS static library. To cross-build the CLI for Linux amd64 + arm64 via
Docker, use ./build-linux.sh.
flextunnel generate-iroh-key -o server.key # prints the server's EndpointId
flextunnel show-iroh-id --secret-file server.key # re-print the EndpointIdClient authentication keys are managed by the standalone
flexaccess-keys CLI;
install it with its one-line installer
(curl -sSL https://flexaccessdev.github.io/flexaccess-keys/install.sh | bash,
Windows: irm https://flexaccessdev.github.io/flexaccess-keys/install.ps1 | iex):
# On each client machine: generate that client's keypair, then derive its
# public authorized-key entry. Send only the public entry to the server admin.
flexaccess-keys generate-auth-key "alice laptop" -o client.key
flexaccess-keys show-auth-key --private-key-file client.keyWithout -o, the generate commands print the key file to stdout instead.
Keep server.key and each client.key private (written 0600 on Unix; both
key files carry # Created: / # Public key: comments). Share
the server's EndpointId with clients, and collect each client's printed
authorized-key entry (ed25519-pub:… — not a secret) into the server's
authorized-keys file, one per line with an optional trailing comment, ssh
authorized_keys style:
# ./authorized_keys
ed25519-pub:XXXXXXXX alice laptop
ed25519-pub:YYYYYYYY build server
All commands above accept --json for machine-readable output (QA automation).
The routed set is required and is configured in server.toml. This example is a
full tunnel; narrow it later with specific domains/CIDRs if you want split
tunneling.
secret_file = "./server.key"
# Authorized client public keys — always a file (ssh authorized_keys style).
authorized_keys_file = "./authorized_keys"
routed_domains = ["*"]
routed_cidrs = ["0.0.0.0/0", "::/0"]
[host_aliases]
"server.internal" = "127.0.0.1"flextunnel server start -c server.tomlIt prints flextunnel server Node ID: <ENDPOINT_ID> — give that to clients.
flextunnel client start \
--server-node-id <ENDPOINT_ID> \
--auth-key-file client.key \
--socks-port 1080 # SOCKS5 on 127.0.0.1:1080 (loopback only)Point any SOCKS5 client at 127.0.0.1:1080. Use socks5h:// so routed
hostnames reach flextunnel as names and are resolved on the server side:
# With the full-tunnel routed set above, DNS + connect happen server-side.
curl -x socks5h://127.0.0.1:1080 https://example.com
# a server-side host alias, for example server.internal -> 127.0.0.1
curl -x socks5h://127.0.0.1:1080 http://server.internal:8000/
# SSH through the proxy
ssh -o ProxyCommand='nc -X 5 -x 127.0.0.1:1080 %h %p' user@internal-hostFor a throwaway "route everything through this box for a few minutes" session,
--quick skips all of the above — no key files, no config. There is
no keypair auth at all: each side enters the other side's EndpointId. The
client's id becomes the server's one-entry allowlist (enforced natively at the
TLS handshake, like bridge allowlisting), and dialing the server's id is itself
what authenticates the server:
# On the client host: prints this client's EndpointId (enter it on the server)
# and prompts for the server EndpointId, then shows a live control panel in
# this terminal. Needs an interactive terminal.
flextunnel client start --quick
# On the server host: prompts for the client EndpointId printed above and
# allowlists it as the only allowed client, then prints this server's
# EndpointId to enter at the client prompt. Full-tunnels all traffic; exits on
# its own if the client doesn't connect within 5 minutes. Nothing is persisted.
flextunnel server start --quickEither side can start first — both block at their prompt until the other's id is entered.
The quick server runs a full tunnel (routed_domains = ["*"],
routed_cidrs = ["0.0.0.0/0", "::/0"]); enter a SOCKS5/HTTP port at the client
prompt to open a local proxy listener. Both sides are fully ephemeral: neither
takes the single-instance lock (so a quick session can run alongside a real one,
or another quick one), and both forget everything on exit.
The quick client is self-contained: after the prompt it runs the same live
panel as client control right in that terminal — but it
opens no control socket (nothing else can attach to it), and quitting the
panel (q) disconnects the tunnel and exits, rather than detaching. A
quick client reads no config, so it has no port forwards.
From a connected client, flextunnel.internal is reserved by flextunnel and is
always tunneled to the server, regardless of the routed set. The browser view is
HTML; /status.txt is plain text and /status.json is structured JSON for
scripts:
# plain-text status through the default SOCKS5 listener
curl -sS -x socks5h://127.0.0.1:1080 http://flextunnel.internal/status.txt
# JSON status through the default SOCKS5 listener
curl -sS -x socks5h://127.0.0.1:1080 http://flextunnel.internal/status.json
# same endpoint through the optional HTTP proxy listener
curl -sS -x http://127.0.0.1:8081 http://flextunnel.internal/status.txt
curl -sS -x http://127.0.0.1:8081 http://flextunnel.internal/status.jsonThe JSON response includes version, server_node_id, routed_domains,
routed_cidrs, host_aliases, dns_forwards, bridges,
inbound_bridges, and duplicate-id blocklist counts under
duplicate_id_blocklist.
For more ways to use the proxy — curl/git/browser recipes, ssh through the
tunnel, and putting a plain local TCP port in front of it for apps that can't
speak SOCKS5 (databases, RDP, most GUIs) — see
docs/proxy-usage.md.
Add --http-port <PORT> to also run an HTTP proxy alongside SOCKS5 — useful
for the many tools that only speak an HTTP proxy or whose SOCKS5 support
resolves DNS client-side (wget, Docker pulls, npm/yarn, JVM/JDBC). It
handles HTTPS (and any TCP) via CONNECT tunneling and plain-HTTP via
absolute-URI forwarding; either way the hostname goes to the proxy, so DNS
still happens on the server.
flextunnel client start \
--server-node-id <ENDPOINT_ID> \
--auth-key-file client.key \
--socks-port 1080 \
--http-port 8081
# HTTPS tunnels via CONNECT; plain HTTP is forwarded
https_proxy=http://127.0.0.1:8081 curl https://example.com
http_proxy=http://127.0.0.1:8081 curl http://example.comSee docs/http-proxy-roadmap.md for the gap
analysis and what it doesn't cover (raw-TCP apps still need SOCKS5 or socat).
| Command | Description |
|---|---|
server start |
Run the proxy server. |
client start |
Run the proxy client (optional SOCKS5 and HTTP proxy listeners, plus the port forwards declared in its config). |
client control |
Attach the read-only terminal control panel to a running client. |
client help |
Show the client subcommands and their help. |
generate-iroh-key [-o <FILE>] [--force] [--json] |
Generate the server's iroh identity key (stdout without -o). |
show-iroh-id --secret-file <FILE> [--json] |
Print the iroh id (EndpointId) for a key. |
Client auth keypairs are generated with the standalone
flexaccess-keys CLI
(generate-auth-key / show-auth-key), not by flextunnel itself.
| Flag | Description |
|---|---|
-c, --config <FILE> |
Load options from a TOML file (CLI flags override it). |
--default-config |
Load ~/.config/flextunnel/server.toml. |
--secret-file <FILE> |
Server identity key. |
--authorized-keys-file <FILE> |
File of authorized client public keys, one ed25519-pub:… per line (optional trailing comment, ssh authorized_keys style). |
--relay-url <URL> |
Custom relay URLs (repeatable; at least two distinct relays, since the server rides out a relay outage by moving onto another one). Configuring custom relays disables n0 internet discovery: clients reach this server via relay hints, and outbound bridges attach the same hints when dialing peer servers. mDNS local discovery stays on. |
--relay-auth-token <TOKEN> |
Shared bearer token sent to every custom relay's WebSocket upgrade. Only valid with --relay-url (rejected with the default relays). |
--quick |
Ephemeral one-off server: prompt for the client's EndpointId (shown by client start --quick) and natively allowlist it as the only allowed client — no auth keypair — then mint an in-memory identity, full-tunnel all traffic, print this server's EndpointId, and exit if the client doesn't connect within 5 minutes. Needs an interactive terminal. Takes no single-instance lock; nothing is persisted. Conflicts with -c/--secret-file/--authorized-keys-file. |
| Flag | Description |
|---|---|
-c, --config <FILE> |
Load options from a TOML file (CLI flags override it). Without it, ~/.config/flextunnel/client.toml is used if present. |
-n, --server-node-id <ID> |
Server EndpointId. |
--socks-port <PORT> |
Optional SOCKS5 listener port, e.g. 1080. Binds 127.0.0.1 only. Disabled unless set. |
--http-port <PORT> |
Optional HTTP proxy listener port (CONNECT + plain-HTTP forwarding). Binds 127.0.0.1 only. |
--auth-key <SECRET> / --auth-key-file <FILE> |
Client auth keypair (one required): the inline ed25519-sec:… secret, or the key file from flexaccess-keys generate-auth-key. |
--relay-url <URL> |
Custom relay URLs (repeatable; at least two distinct relays, the same set as the server). Configuring custom relays disables n0 internet discovery (the server is reached via relay hints); mDNS local discovery stays on. |
--relay-auth-token <TOKEN> |
Shared bearer token sent to every custom relay's WebSocket upgrade. Only valid with --relay-url (rejected with the default relays). |
--auto-reconnect |
Force auto-reconnect on (overrides auto_reconnect = false in the config). |
--no-auto-reconnect |
Exit on the first failed connection attempt or drop instead of retrying. |
--max-reconnect-attempts <N> |
Cap consecutive retries before giving up (unlimited if unset). |
--quick |
Self-contained ephemeral session (pairs with server start --quick): ignore any saved config, print this client's EndpointId (enter it at the quick server's prompt — that allowlist entry is the credential; no auth keypair), prompt for the server EndpointId, then run the live control panel in this terminal. Needs an interactive terminal. Takes no lock and opens no control socket; quitting the panel disconnects. Nothing is persisted. Conflicts with -c/--auth-key(-file). |
flextunnel client start needs at least one flag — run with no arguments and it
prints help. With a flag but no -c, it loads ~/.config/flextunnel/client.toml
if it exists (so flextunnel client start --socks-port 1080 runs off the default
config). --quick ignores any saved config, prints this client's EndpointId
(its credential — enter it on the quick server; there is no auth keypair), and
prompts (on an interactive terminal) for the server EndpointId and optional
proxy ports, then
runs the self-contained control panel described under client control
right in that terminal — but with no control socket exposed, and quitting the
panel disconnects instead of detaching. Nothing is saved.
Port forwards are declared in the config as [[forwards]] tables (see
client.toml.example): each listens on
localhost:<local_port> and opens a server-direct stream to
<remote_host>:<remote_port> on the authenticated connection (the server
enforces its routed set and resolves the host). They are validated at startup
(nonzero ports, valid host, unique local ports) and all come up with the
client. There is no CLI flag and nothing can be changed live: to change the
set, edit the config and restart the client.
With neither --socks-port nor --http-port (nor the config keys) the
client runs in port-forward-only mode: it holds the tunnel and serves only
the control panel and the port forwards.
A client's on-disk identity — the single-instance lock and the control
socket — is keyed by the prefix of its server_node_id (which never changes
for a profile). So one client runs per server per user, and clients for
different servers coexist without any extra configuration. The optional name
key in the config ("aws", "home network") is a display-only label shown in the
control panel. (A --quick client is exempt: it takes neither — no lock, no
socket.)
flextunnel client control # profile from ~/.config/flextunnel/client.toml
flextunnel client control -c aws.toml # profile from a specific config
flextunnel client control -n <ENDPOINT_ID> # or by server id directlyAttaches a read-only terminal control panel to the running client for
a profile, over its control socket
(~/.config/flextunnel/client-<server id prefix>.sock; a named pipe on
Windows). It shows live status — connection phase and uptime, server/client
node ids, connection paths (direct/relay), the server-pushed routing breakdown
(split-tunnel rules, host aliases, DNS forwards, and bridge routes), and the
port forwards declared in the client's config with their live state
(listening, active connections, or switched off with the bind-failure reason).
Nothing about the client can be changed from the panel: the forward set is fixed by the config for the life of the client (edit the config and restart to change it), and the channel carries no mutations. It runs as the same user as the client — no elevated privilege on either end.
Detaching (q) never affects the tunnel; several panels can attach at once.
Instead of passing everything on the command line, server start and
client start can read a TOML file:
flextunnel server start -c server.toml
flextunnel client start -c client.toml
flextunnel client start --socks-port 1080 # loads ~/.config/flextunnel/client.tomlPrecedence is CLI flag > config file > built-in default, so you can keep a
file and override settings on the command line. Credential groups are replaced
as a unit: for example, if the CLI supplies either --auth-key or
--auth-key-file, the config file's auth_key_file is ignored. Unknown
or misspelled keys are rejected (deny_unknown_fields) rather than silently
ignored. Paths support ~ expansion.
See server.toml.example and
client.toml.example for the full set of keys. A minimal
client file:
server_node_id = "<server endpoint id>"
socks_port = 1080 # SOCKS5 on 127.0.0.1:1080 (loopback only)
auth_key_file = "~/.config/flextunnel/client.key"Private keys are always file references in a config (secret_file,
auth_key_file) — there are no inline-key config keys. CLI flags still work
and override any of these (--auth-key exists only as a CLI flag).
Server-only routing keys include host_aliases,
dns_forwards, outbound [bridges.<name>], and inbound
allowed_bridge_servers; these are config-file only because they describe the
server's routing policy. Bridges carry no keypair: a bridging server
authenticates by its TLS-authenticated endpoint id, which the receiving server
must list in allowed_bridge_servers (enforced natively at the handshake).
The server config can map hostnames to addresses on its own network, so a client
can reach the server's loopback or internal hosts by a real name. Add a
[host_aliases] table to server.toml (config-file only — there is no CLI flag):
[host_aliases]
"server.internal" = "127.0.0.1" # the server's own loopback
"node2.internal" = "192.168.1.50" # another host on the server's networkWhen a requested hostname matches a key (case-insensitive), the server rewrites it to the value — an IP or another hostname — keeping the requested port, then resolves and connects like any other target. Only domain targets are aliased; literal IPs pass through unchanged.
This is also the clean way around Firefox refusing to proxy literal
localhost / 127.0.0.1: alias server.internal → 127.0.0.1 on the server and
browse to http://server.internal:8000/. Use socks5h:// (or set Firefox's
network.proxy.socks_remote_dns = true) so the name is resolved by the server,
not locally.
The routed set (the tunnel set) is a VPN-style split-tunnel "included routes" list that decides which destinations traverse the tunnel. Targets not on it are not rejected — the client falls back to a direct connection for them. It is useful when a client must send all its traffic to the local SOCKS5 proxy (e.g. an iOS WebView, whose proxy config is global) but only some hosts should actually be tunneled. It is required and configured on the server only (config-file only — there is no CLI flag); the client configures nothing:
# server.toml
routed_domains = ["*.example.com", "httpbin.org"]
routed_cidrs = ["10.0.0.0/8", "192.168.1.5"]The tunnel set is required: a server started with an empty set refuses to start, and a client that receives an empty set from a (misconfigured or old) server aborts the handshake rather than silently direct-connecting everything. To route all traffic through the tunnel (full tunnel), use the catch-alls:
routed_domains = ["*"]
routed_cidrs = ["0.0.0.0/0", "::/0"]The server is the single source of truth. It pushes the list to every client in the handshake response, so there is no client list to keep in sync:
- Client — on connect it learns the server's list. It tunnels only matching
targets and connects everything else directly from its own network
(split-tunneling). The direct path is independent of the tunnel, so off-list
targets keep connecting even while the tunnel is down; an on-list target during
a drop/backoff is held for the client's own reconnect (up to 45s, deploy-style
connection holding) and proceeds transparently once the link is back — only a
reconnect that never lands within the hold gets a network-unreachable failure
(SOCKS5 reply
0x03; the HTTP front-end maps it to502 Bad Gateway). - Server — it also enforces the same list independently as a whitelist,
rejecting any tunnel request for a target not on it (SOCKS5 reply
0x02). This is a defense-in-depth boundary against a misconfigured or untrusted client. (Note the asymmetry: the client falls back to a direct connection for off-list targets, whereas the server rejects them outright.)
Matching: domain entries are exact (example.com), wildcard (*.example.com,
which matches subdomains only — not the bare apex), or * (matches every
hostname), case-insensitive; CIDR entries match IP targets, accept a bare IP as a
single host, and a default route (0.0.0.0/0 / ::/0) matches every IP.
Hostnames are matched only against routed_domains and IPs only against
routed_cidrs. A numeric IP literal is always gated by routed_cidrs even
when a client sends it in hostname form (SOCKS5 ATYP_DOMAIN), so * never lets
a raw IP through — it can only route real hostnames.
Only the combined set must be non-empty — setting just one list is fine. The
two never cross: an omitted/empty list means that whole category is off-list and
always direct-connected. So routed_domains alone (no routed_cidrs)
tunnels those hostnames but direct-connects every bare-IP target, and
routed_cidrs alone tunnels those IPs but direct-connects every hostname.
- Client blocking mode. Today the client always direct-connects every
off-list target (split-tunneling), and this is
the same for the desktop and iOS clients (they share the same core). A future
client option — likely
routed_mode = "block" | "direct"(default"direct") — will let a client instead refuse an off-list connection, returning a SOCKS5 error to the local app rather than falling back to a direct connection. This is aimed mainly at the desktop client, where blocking off-list traffic can be preferable to letting it leak out directly; the iOS client keeps defaulting to direct-connect. (The server's0x02rejection above is a separate, server-side control and is unaffected.)
Auto-reconnect is enabled by default (auto_reconnect = true); pass
--no-auto-reconnect (or set auto_reconnect = false) to disable it, and
--auto-reconnect to force it on over a config that disabled it.
- A failed connection attempt — the first one included — or a lost
connection is retried with exponential backoff + jitter (1s doubling to
60s), indefinitely, unless
--max-reconnect-attemptscaps it or auto-reconnect is disabled. A server that is down, or not up yet, is the ordinary case, not a reason to exit: the client waits it out and connects when the server appears. - A long outage is cheap to sit through: once the backoff reaches its cap the
client makes one bounded connect attempt a minute. Repeated
failures escalate to rebuilding the iroh endpoint from scratch after the
third one, and then at most every 30 minutes for as long as the outage
lasts (see
docs/architecture.md). - A permanent error (a rejected key, a malformed config) never retries; that is the only kind of error the client exits on.
- The control panel shows the outage's progress: failed attempts so far, the last error, and when the next attempt is due.
- The local proxy listeners stay bound across reconnects. Off-list targets keep connecting directly; on-list requests are held for the reconnect (up to 45s) and only then fail with a network-unreachable reply.
A server with custom relays watches its own home-relay registration: if it
has no connected home relay for 60s and iroh has not re-homed it on its own,
it takes the wedged relay out of its relay map and homes on another configured
relay in place (same server id, same sockets, nothing dropped), so clients off
the LAN (the iOS app) are not stranded until someone restarts the service. The
relay is put back once it is connectable again. A custom relay set is
therefore at least two distinct relays. See
docs/architecture.md.
Logging uses env_logger. The default is info with iroh/tracing quieted to
warn. Override with RUST_LOG, e.g. RUST_LOG=flextunnel=debug.
- iroh-common-architecture — the iroh transport layer shared with tunnel-rs and ezvpn: relays and address lookup (default vs custom relays, relay hints, the per-relay startup probe, relay auth tokens) and self-hosting (running your own iroh relay).
- flexaccess-keys — the
app-independent Ed25519 key format and tooling shared with tunnel-rs: the
ed25519-sec:/ed25519-pub:tokens, key files, authorized-keys documents (key-format specification), and thegenerate-auth-key/show-auth-keyCLI. flextunnel links against the same crate for parsing and verification and retains only its own domain-separated authentication transcript. docs/architecture.md— how it works: connection lifecycle (fixed ALPN, auth handshake, per-stream protocol), module map, concurrency model, reconnect policy, security boundaries, and reference constants.docs/local-ci.md— running the CI workflow's clippy and test steps locally on all three host platforms (macOS natively, Linux and Windows over ssh) against the working tree, viaci/all.sh.docs/systemd.md— running the CLI client under systemd: one template-unit instance per server, whyRestart=on-failureis the whole restart policy, attaching the control panel, journald logging.docs/http-proxy-roadmap.md— the HTTP proxy front-end (CONNECT tunneling + absolute-URI forwarding): motivation, design, and remaining hardening work.docs/proxy-usage.md— using the SOCKS5 and HTTP proxies: which listener a tool needs, native SOCKS5 clients (curl,git, browsers), HTTP-proxy-only tools (wget, Docker, JVM/JDBC),sshthrough the tunnel, andsocat/ssh -L/-Dforwards for apps that speak neither.
flextunnel is modeled on the sibling project ezvpn (an IP-over-QUIC VPN), reusing its iroh transport and secret-key identity (client access control is flextunnel's own ed25519 keypair scheme). The difference: ezvpn creates a TUN device and ships IP packets over unreliable QUIC datagrams (and needs root); flextunnel exposes SOCKS5/HTTP proxy listeners and tunnels TCP over reliable QUIC streams (and needs no root).