nextdesk

Per-user VPN gateway

The VPN catalog app gives each user their own corporate VPN tunnel that all of their other sessions can share — an overlay network per user, without any privileged containers.

How it works

┌────────────────────── per-user Docker network lwp-vpn-<uid> ────────────────┐
│                                                                             │
│  ┌ VPN gateway (alias: vpn) ─────────┐      ┌ Firefox session ┐             │
│  │ GTK4/libadwaita GUI ── openconnect│◄─────│ SOCKS5 vpn:1080 │             │
│  │            └── ocproxy :1080      │      └─────────────────┘             │
│  │        (userspace lwIP, no tun,   │      ┌ Terminal session ┐            │
│  │         no NET_ADMIN, no root)    │◄─────│ ALL_PROXY / ssh  │            │
│  └───────────────────────────────────┘      └──────────────────┘            │
└─────────────────────────────────────────────────────────────────────────────┘

User flow

  1. Launch VPN from the catalog and log in (password + TOTP).
  2. On connect the window minimises itself; the taskbar shows a shield: pulsing amber = gateway open but not connected, green = tunnel up. Click the shield to bring the window back (status, reconnect, or click Disconnect).
  3. Launch your apps. Sessions started while the VPN is up get a local SOCKS5 relay (lwp-vpn-relay.py, 127.0.0.1:1081) and are wired to it automatically:

    Client Mechanism
    curl, git, most CLIs ALL_PROXY=socks5h://127.0.0.1:1081 (+ NO_PROXY for LWP internals)
    Chromium-based (Vivaldi, …) SOCKS_SERVER / SOCKS_VERSION env (Chromium ignores socks5h:// in all_proxy)
    Firefox start wrapper writes distribution/policies.json (SOCKS5 + proxy-DNS, not locked)
    ssh (Terminal app) ssh_config.d drop-in: ProxyCommand nc -X 5 -x 127.0.0.1:1081 %h %p, active only when LWP_VPN_PROXY is set. Bypass per host: ssh -o ProxyCommand=none <host>

    Anything else: configure SOCKS5 host 127.0.0.1, port 1081 manually, and enable proxy DNS so hostnames resolve wherever the relay dials.

  4. Per-window VPN toggle. Each such window shows a shield button in its titlebar. Per connection the relay either dials direct (shield grey/off — the default, so apps that need plain internet keep working) or chains to the gateway at vpn:1080 with tunnel DNS (shield green/on). Flip it any time — the relay polls GET /api/sessions/vpn/mode with its session token every 2 s, and on a change it drops all open connections (browsers hold keep-alive/HTTP2 pools for minutes and would otherwise stay on the old path), so everything reconnects the new way within seconds.

    The first time a user’s tunnel comes up, a one-time help dialog explains the shield and the env vars (reopen it: right-click the taskbar shield).

  5. Per-app defaults, set by the user. Profile → App VPN defaults lets each user choose per app: start direct (default), start through the VPN, or never proxied. This writes LWP_VPN_DEFAULT / LWP_VPN_EXEMPT into preferences.app_env[app_id]; the backend merges these whitelisted keys over the admin’s app env at session launch (users cannot set other env vars). Apps that should start tunneled can set LWP_VPN_DEFAULT=on in their env_json (Admin → Apps).

Sessions that were already running when the VPN came up are late-joined to the network (they can reach vpn:1080 for manual proxy config), but env-based auto-proxying requires relaunching the app — environment variables cannot be injected into a running container.

Admin configuration

Preset the portal in Admin → Apps → VPN → env vars (env_json):

Var Purpose Default
LWP_VPN_ROLE gateway — marks the app for the orchestrator (don’t change) gateway
LWP_VPN_DEFAULT on client apps’ env_json: on starts their window with VPN routing enabled off (direct)
LWP_VPN_EXEMPT on client apps’ env_json: 1 — never inject proxy env into this app (for apps that misbehave when SOCKS env is merely present, e.g. Ferdium); no shield toggle unset

Users can override LWP_VPN_DEFAULT / LWP_VPN_EXEMPT per app for themselves in Profile → App VPN defaults (whitelisted keys only). | LWP_VPN_SERVER | portal/gateway hostname users connect to | prompt | | LWP_VPN_USER | username preset (users can override at the prompt) | prompt | | LWP_VPN_PROTOCOL | any openconnect --protocol value: gp, anyconnect, pulse, fortinet, … | gp |

One VPN session per user is enforced (409 on a second launch) — SSO portals typically allow a single concurrent login anyway.

Tunnel state / taskbar indicator

The gateway reports state to the backend with its session token (same pattern as the session recorder):

Kubernetes

Limitations