-
Notifications
You must be signed in to change notification settings - Fork 0
Architecture and Internals
The image is supervised by s6-overlay. Startup is a dependency graph, not a script — fail-closed ordering is enforced structurally:
init-env (oneshot) parse/validate env, parse URL list
init-sysctl (oneshot) IPv6 availability, forwarding sysctls ← after init-env
init-firewall (oneshot) install nft table + sets, gateway rules ← after init-sysctl
init-routes (oneshot) NETWORK/NETWORK6 return routes ← after init-firewall
svc-openconnect (longrun) resolve → update endpoint sets → connect ← after init-routes
- A failed oneshot aborts container start (
S6_BEHAVIOUR_IF_STAGE2_FAILS=2) — a config typo or an uninstallable firewall never yields a running-but-unprotected container. -
svc-openconnectis the only long-running process; the image is otherwise daemon-free (gateway DNS is pure nftables — see Gateway DNS). - The service's
finishscript implements the exponential backoff (s6 restarts immediately by design, so the delay lives in the service) and never touches the firewall.
| Path | Role |
|---|---|
/usr/local/bin/backend-functions |
env parsing, URL grammar, logging, shared helpers — sourced by everything |
/usr/local/bin/firewall |
install / bootstrap-dns / set-endpoints / post-connect — all set changes are single-transaction (atomic) |
/usr/local/bin/gateway |
setup (forward/NAT/DNS-interception rules) and dns-update (re-point the redirect DNAT on reconnect) |
/usr/local/bin/vpnc-script |
custom, container-aware vpnc-script (below) |
/usr/local/bin/healthcheck |
Docker HEALTHCHECK probe — see Automatic Reconnection |
The stock vpnc-script assumes resolvconf/systemd-ish hosts and misbehaves when Docker bind-mounts /etc/resolv.conf (the file can only be rewritten in place, never replaced). The custom script is ~100 lines of ip/printf, driven by the reason openconnect passes:
-
connect / reconnect: bring up
tun0with the assigned IPv4/IPv6 addresses and MTU (MTUenv > server value > 1406); install0.0.0.0/1+128.0.0.0/1and::/1+8000::/1half-default routes (they beat the existing defaults without touching them — rollback is "interface disappears"), or the server's split routes whenSPLIT_TUNNEL=true; rewrite/etc/resolv.confin place with the pushed (orDNS-overridden) resolvers; close the bootstrap DNS hole; re-point gateway DNS interception; record connect state and reset the failure counter. One structured log line per phase:[vpnc] connect dev=tun0 mtu=... ip4=... ip6=... routes=... dns=.... - disconnect: restore the original resolv.conf content, reopen the bootstrap DNS hole, drop the connect-state file. Routes and addresses vanish with the tun interface; the firewall is never touched.
There is no RA/SLAAC on the tun interface — all IPv6 config is static from the environment openconnect provides (INTERNAL_IP6_ADDRESS/INTERNAL_IP6_NETMASK).
/run/openconnect-client/ (tmpfs, rebuilt each start):
| File | Content |
|---|---|
urls |
parsed failover list, one host port url per line — mode 0600 (contains the camouflage secret) |
url-index |
index of the active URL entry |
failures |
consecutive-failure counter (drives backoff, candidate rotation, failover) |
connected |
ip4=... / ip6=... of the current session; existence = connected |
resolv.conf.orig |
pristine Docker resolv.conf, restored on disconnect |
has-ipv6 |
IPv6 availability verdict from init-sysctl |
- Restore bootstrap resolv.conf if the previous session died uncleanly.
- Pick the active URL; advance after 3 consecutive failures.
- Resolve (hosts-file first, then DNS) per
CONNECT_FAMILY; abort attempt if nothing resolves. - Atomically swap all resolved
ip@portpairs into the endpoint sets. - Build the openconnect command (protocol, trust, auth, DTLS,
--resolve host:ippinning); pipe the password via stdin; filter all output through the secret redactor. - Wait up to 60 s for the vpnc-script to signal connect; enforce
IPV6_MODE=require; optionally dump diagnostics. - On exit:
finishcomputes the backoff and s6 restarts the cycle.
Multi-stage Dockerfile: an openconnect build stage (upstream source, GnuTLS backend, trimmed features — see Building and CI for the why), an s6-overlay fetch stage (arch-mapped release tarballs), a rootfs-assembly stage (permissions normalized once), and a minimal Alpine runtime with pinned APK packages (gnutls, libxml2, lz4-libs, zlib, iproute2-minimal, nftables, curl, bind-tools, openssl, ca-certificates, tzdata).
openconnect-client · MIT License · Built on OpenConnect + s6-overlay
Setup
- Getting Started
- Docker Compose Examples
- Configuration Reference
- Connection URL
- Authentication
- Protocols
Routing traffic
Operating
Under the hood
Help