-
Notifications
You must be signed in to change notification settings - Fork 0
Architecture and Internals
This page describes how the container works internally. Useful for contributors, debugging, and understanding the boot sequence.
The container uses s6-overlay for process supervision. Services start in a defined order:
entrypoint (container start)
│
├─ init-firewall Apply iptables rules (depends on entrypoint backend selection)
├─ init-setupcron Configure cron jobs from RECREATE_VPN_CRON / CHECK_CONNECTION_CRON
│
├─ svc-nordvpn Main WireGuard service (long-running)
└─ svc-cron Cron daemon (long-running)
Location: /usr/local/bin/entrypoint
- Detects kernel version
- Tests nft and legacy iptables backends by toggling a chain policy (
DROP↔ACCEPTonOUTPUT) - If preferred backend fails, tests the fallback
- If legacy is selected and nft tables already contain rules, flushes nft tables to avoid mixed stacks
- Exports selected backends (
IPT,IP6T) to/run/xt/backend.env - Sets
INPUT,OUTPUT,FORWARDtoDROPon both IPv4 and IPv6 - Allows loopback traffic
Backend preference by kernel:
| Kernel version | Preferred backend | Fallback |
|---|---|---|
| ≥ 4.18 | nft (iptables) |
legacy (iptables-legacy) |
| < 4.18 | legacy (iptables-legacy) |
nft (iptables) |
Location: /usr/local/bin/backend-functions
Sourced by every script. Provides the environment-variable defaults (including dns, network, forward_from, nordvpnapi_ip) and:
-
run4()/run6()— Execute iptables commands with logging (non-fatal) -
run4_critical()/run6_critical()— Execute or sleep forever on failure -
is_vpn_connected()— Checks for thewg0interface -
log()/log_error()/log_warning()— Timestamped logging -
parse_cron()— Converts cron expressions to human-readable descriptions
Location: /usr/local/bin/vpn-config
- Fetches your NordLynx (WireGuard) private key from the NordVPN API using
TOKEN(v1/users/services/credentials), via pinned API IPs (no DNS). The key is not cached — it is re-fetched on every connect. - Resolves COUNTRY/CITY/GROUP to numeric IDs using the JSON data files
- Builds the NordVPN API query (always filtered to NordLynx, tech id 35); CITY uses the
country_city_idfilter - Fetches the server list using pinned API IPs (no DNS)
- Detects specific server hostnames and gives them
load=0 - Sorts by load (multi-location) or keeps API order (single location)
- Applies
RANDOM_TOPif set - Writes the selected server's WireGuard config to
/etc/wireguard/wg0.conf(private key,Address,[Peer]endpoint + public key,AllowedIPs = 0.0.0.0/0,PersistentKeepalive = 25)
Location: /etc/s6-overlay/s6-rc.d/svc-nordvpn/run
- Calls
vpn-configto generatewg0.conf - Adds a temporary pinhole in the
VPN-SERVERchain for the server IP (UDP/51820 on eth0) - Brings the tunnel up with
wg-quick up wg0 - Writes
/etc/resolv.conffrom$dns(Docker's embedded resolver is unreachable behind the kill switch) - Waits for the connection (checks
wg0, up to ~60 seconds) - Optionally runs network diagnostics (
NETWORK_DIAGNOSTIC_ENABLED) - Blocks (
sleep infinity) to keep the service alive
The WireGuard config intentionally omits a
DNS =line —wg-quick'sresolvconfstep fails inside Docker, so DNS is managed directly (step 4) instead.
Brings wg0 down and flushes the VPN-SERVER chain so a fresh connect starts clean.
Location: /usr/local/bin/vpn-healthcheck
- Sends HTTP requests to the configured URL(s)
- Retries
CHECK_CONNECTION_ATTEMPTStimes with configurable interval - If all fail, calls
vpn-reconnect
Location: /usr/local/bin/vpn-reconnect
- Stops
svc-nordvpnvia s6-rc - Waits briefly
- Restarts
svc-nordvpn(which re-fetches the key and picks a new server)
Location: /usr/local/bin/network-diagnostic
Two modes:
-
--basic: Public IP + geolocation only -
--full(default): Complete diagnostics including interfaces, iptables rules, DNS, routes, WireGuard status, and kernel version
Located in /usr/local/share/nordvpn/data/:
| File | Purpose |
|---|---|
countries.json |
Country name/code/ID mappings |
groups.json |
Server group definitions |
technologies.json |
VPN technology definitions |
Origin of these files: They are generated from the NordVPN public API (
https://api.nordvpn.com/), not maintained by hand, and are refreshed automatically by themaintenance-updatesGitHub Actions workflow, which opens a pull request when NordVPN changes its API schema. Do not edit them by hand — manual changes are overwritten the next time the workflow runs.
| Path | Purpose |
|---|---|
/run/xt/backend.env |
Selected iptables backend (IPT, IP6T) |
/etc/wireguard/wg0.conf |
Current server's WireGuard config (private key, peer, endpoint) |
WireGuard is a silent protocol with no management socket. Status comes from the kernel:
-
wg show wg0— peer endpoint, last handshake, transfer counters -
is_vpn_connected()checks only that thewg0link exists; a real connection also needs a recent handshake and non-zeroreceivedbytes
The entrypoint script runs first and:
- Selects the iptables backend (nft or legacy — see Firewall Backends)
- Sets
INPUT,OUTPUT, andFORWARDpolicies toDROPon both IPv4 and IPv6 - Allows loopback traffic (required for inter-process communication)
At this point, all network traffic is blocked.
The init-firewall service then:
- Detects the Docker network (eth0 subnet and gateway)
- Enables connection tracking (ESTABLISHED/RELATED)
- Sets up MASQUERADE on the
wg0(VPN) interface - Creates a
VPN-SERVERchain and jumps eth0 UDP/51820 to it - Adds NordVPN API IP exceptions (TCP/443 only) from
NORDVPNAPI_IP - If
NETWORKis set, adds static routes and bidirectional allow rules for those CIDRs - If
FORWARD_FROMis set, opensFORWARDfor those CIDRs overwg0(see VPN Gateway Mode)
When connecting:
- The VPN server IP gets a temporary rule in the
VPN-SERVERchain (UDP/51820 on eth0) - When the connection drops,
svc-nordvpn/finishflushes that chain
INPUT chain: ACCEPT lo → ACCEPT ESTABLISHED,RELATED → [NETWORK CIDRs] → DROP
OUTPUT chain: ACCEPT lo → ACCEPT ESTABLISHED,RELATED → ACCEPT wg0 → VPN-SERVER (eth0 udp/51820) → [NORDVPNAPI IPs] → [NETWORK CIDRs] → DROP
FORWARD chain: ACCEPT ESTABLISHED,RELATED → [FORWARD_FROM CIDRs over wg0] → DROP
VPN-SERVER chain: [temporary rule for the current VPN server IP]
NAT/POSTROUTING: MASQUERADE on wg0
Configuration
- Server Selection
- Server Groups
- IPv6 Configuration
- Automatic Reconnection
- Local Network Access
- VPN Gateway Mode
- Custom DNS
- Permissions
Security
Examples
Operations
Reference