Tundler ("tunnel bundler") packages a small REST API in a Docker image to manage multiple VPN providers. It can rotate tunnels on demand and exposes an HTTP proxy routed through the active VPN.
Unlike other solutions, it depends as much as possible on the VPN providers’ official client libraries to minimise breakage and remains stateless on its own.
- REST API on port
4242
for controlling VPN connections. - ExpressVPN, Mullvad, NordVPN and Private Internet Access (PIA) support out of the box.
- Optional HTTP proxy on port
8484
with Envoy-based HTTP/HTTPS support. - YAML configuration file for location filtering and debug mode.
- Easily extensible to add new providers.
Tundler uses Linux network namespaces to provide VPN proxy functionality while maintaining API accessibility. The system consists of two isolated network environments within a Docker container:
HOST SYSTEM
┌────────────────────────────────────────────────────────────┐
│ curl --proxy localhost:8484 example.com │
└──────────────────────┬─────────────────────────────────────┘
│ HTTP/HTTPS requests
▼
┌────────────────────────────────────────────────────────────────────┐
│ DOCKER CONTAINER │
│ │
│ ┌───────────────── DEFAULT NAMESPACE ────────────────────────┐ │
│ │ │ │
│ │ ┌─────────────┐ ┌─────────────────┐ │ │
│ │ │ Tundler API │ │ Envoy Proxy │ │ │
│ │ │ :4242 │ │ :8484 │ │ │
│ │ └─────────────┘ └─────────────────┘ │ │
│ │ │ │ │
│ │ │ fwmark 200 │ │
│ │ │ (policy routing) │ │
│ │ ▼ │ │
│ │ ┌─────────────────────────────────────┐ │ │
│ │ │ vpn-host │ │ │
│ │ │ 172.18.0.1/30 │ │ │
│ │ │ (veth interface) │ │ │
│ │ └─────────────────┬───────────────────┘ │ │
│ └──────────────────────────┼─────────────────────────────────┘ │
│ │ virtual ethernet pair │
│ ▼ │
│ ┌──────────────── VPN NAMESPACE (vpnns) ─────────────────────┐ │
│ │ │ │
│ │ ┌─────────────────────────────────────┐ │ │
│ │ │ vpn-ns │ │ │
│ │ │ 172.18.0.2/30 │ │ │
│ │ │ (veth interface) │ │ │
│ │ └─────────────────────────────────────┘ │ │
│ │ │ │
│ │ ┌──────────────┐ ┌────────────┐ ┌──────────────┐ │ │
│ │ │ ExpressVPN │ │ ... │ │ NordVPN │ │ │
│ │ │ Service │ │ │ │ Service │ │ │
│ │ │ │ │ │ │ │ │ │
│ │ │ ┌────────┐ │ │ │ │ ┌────────┐ │ │ │
│ │ │ │ tun0 │ │ │ │ │ │nordlynx│ │ │ │
│ │ │ │ IF │ │ │ │ │ │ IF │ │ │ │
│ │ │ └────────┘ │ │ │ │ └────────┘ │ │ │
│ │ └──────────────┘ └────────────┘ └──────────────┘ │ │
│ │ │ │ │
│ └────────────────────────────────┼───────────────────────────┘ │
│ │ VPN tunnel to internet │
│ ▼ │
└────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────┐
│ INTERNET │
│ (VPN Server) │
└─────────────────┘
docker/build.sh
EXPRESSVPN_ACTIVATION_CODE=<code> \
MULLVAD_ACCOUNT_NUMBER=<account> \
NORDVPN_TOKEN=<token> \
PRIVATEINTERNETACCESS_USERNAME=<username> \
PRIVATEINTERNETACCESS_PASSWORD=<password> \
docker/run.sh
The API will be reachable on port 4242
and the HTTP proxy on 8484
.
By default, VPN providers run inside their own network namespace.
The TUNDLER_NETNS
environment variable specifies the namespace name
(defaults to vpnns
). VPN daemons are launched in that namespace using
systemd overrides, while the REST API and proxy stay in the main namespace so they
remain reachable even when the VPN changes routing.
Provider | Variables |
---|---|
ExpressVPN | EXPRESSVPN_ACTIVATION_CODE |
Mullvad | MULLVAD_ACCOUNT_NUMBER |
NordVPN | NORDVPN_TOKEN |
Private Internet Access (PIA) | PRIVATEINTERNETACCESS_USERNAME , PRIVATEINTERNETACCESS_PASSWORD |
When present, ~/.config/tundler/tundler.yaml
is read at startup:
debug: true
providers:
- nordvpn:
locations:
- France
- Germany
debug
enables verbose logging and may also be set with-d/--debug
.providers.<name>.locations
restricts the random locations used whenlocation
is omitted in API calls.login
automatically authenticates a comma-separated list of providers at startup (all
for every provider).
Endpoint | Method | Query params | Description |
---|---|---|---|
/ |
GET | – | List providers and login state |
/connect |
POST | location , provider (optional) |
Connect to a new location or provider |
/disconnect |
POST | – | Tear down the current tunnel |
/login |
POST | providers (optional) |
Login comma-separated providers or all when omitted |
/logout |
POST | provider (optional) |
Logout one provider or all when omitted |
/status |
GET | – | Return tunnel state, IP and provider in use |
- Copy
docker/providers/nordvpn
todocker/providers/<your_provider>
. - Implement the
install.sh
andconfigure.sh
scripts for your provider. - Copy
internal/provider/nordvpn
tointernal/provider/<your_provider>
and implement the interface. - Add a blank import in
internal/provider/register/register.go
:
import _ "github.com/laurentpellegrino/tundler/internal/provider/<your_provider>"
- Document new environment variables in this README.
Pull requests are welcome.