A NetBird exit node that leaves through a commercial VPN.
Self-hosted. It uses Mullvad, PIA or NordVPN directly, or gluetun for
FastestVPN, IVPN, Mullvad, NordVPN, Surfshark and Windscribe. Routing
fails closed, and a small web panel switches servers.
Setup · Requirements · Testing · The panel · Architecture · Changelog
Phones can usually run only one VPN at a time, so turning on Mullvad, PIA or NordVPN means turning off NetBird. Molebridge moves the commercial VPN tunnel onto a machine you keep running. That machine joins your NetBird network as an exit node. Your devices stay on NetBird, select the exit, and their Internet traffic leaves through the VPN provider.
By default Molebridge builds the tunnel itself (Mullvad, PIA, NordVPN). An experimental second backend hands the tunnel to gluetun instead, and Switchyard then chooses servers for FastestVPN, IVPN, Mullvad, NordVPN, Surfshark and Windscribe through it (the gluetun form). Either way, the devices using the exit are NetBird peers.
Separately, an experimental gluetun sidecar lets Switchyard choose and check the server of an ordinary gluetun container that your other containers share, with no NetBird involved. The experimental native sidecar, new in 0.9.0, gives those containers a tunnel that Molebridge builds itself, with PIA over WireGuard among the providers, and keeps their traffic in it with its own routing contract and firewall; it has run live once, with PIA on Kubernetes (testing).
You choose the server (Mullvad, NordVPN, the gluetun providers) or region (PIA) in a small web panel called Switchyard. Every device using the exit follows the switch without any client changes. Alternatively, set SERVER to keep the exit on a configured server; the panel then shows it read-only. Connections open through the exit drop when it switches.
0.8.0 is a rewrite of 0.7 for adoption: you pull one signed image and run it.
- One image, one binary.
ghcr.io/irashack/molebridge(amd64 and arm64) holds one Go binary,molebridge, and the pinned NetBird client. The first argument picks the role:exit,panel,health,status,check-config,prepare-config,nordvpn-key,gluetun-auth,gluetun-post-rulesandfix-blacklist. GitHub Actions builds and signs the image with cosign, with build provenance and SBOMs, and promotes a release only after a maintainer's live-acceptance run attested that the exact image passed (verification of a release). - NetBird runs inside the exit container. A native exit is two containers
(
exitandswitchyard) where 0.7 had four.molebridge exitis PID 1 and supervises the worker and NetBird (how it works). - Nothing to build, no Python on the host. The Python applier and panel and
the routing shell scripts are gone. The setup tools run from the same image
as throwaway containers. Only
routing/gluetun-preflightremains, a shell wrapper that runs inside gluetun's container. - Repair follows the kernel. The exit repairs a changed routing rule, route, link or address when the kernel reports it, and runs a full pass every 30 seconds as the fallback. 0.7 compared everything every 2 seconds. On the one host tested, an idle exit used 0.15 to 0.5 % of one core where 0.7.0 used about 7.9 %, and a deleted rule was back in 8 to 23 ms (resources and drills).
The routing contract, the status record, the state files and the panel's
wording are unchanged, so a 0.7.0 panel reads a 0.8.0 exit. Removed: doctor
and recover, compose.pia.yaml and compose.nordvpn.yaml (one
compose.yaml now takes PROVIDER), and 0.7's standalone gluetun form. The
state files and the NetBird identity carry over from 0.7, the Compose files
don't: follow the upgrade steps. The
full list is in the changelog.
Molebridge 0.8.0 is experimental software with one maintainer, not a 1.0. Testing is the record, and the only place the results are written out in full; in short:
- Tested live at 0.8.0: one Debian host (amd64) running rootless Podman 5.8 with Quadlet units, against a self-hosted NetBird server. Native Mullvad, PIA and NordVPN exits, a gluetun exit and a gluetun sidecar (both with NordVPN) were upgraded from 0.7.0 and came up with their peers and selections. The released image passed rule-drift and tunnel-loss drills on the native exits, run from inside the exit's namespace with no client device held on it.
- Run in CI: unit, fuzz and namespace-drill suites and the release checks on amd64; the image checks on amd64 and arm64. The namespace drills also passed once on an arm64 Linux VM.
- Not tested at 0.8.0: Docker Engine and NetBird Cloud; running the
shipped Compose files on any engine (CI only validates them with
docker compose config; the live exits ran from Quadlet units this repository doesn't ship); installing from the signed bundle on a clean host; phones; a reboot; the panel's sign-in against a real identity provider; PIA port forwarding; gluetun with providers other than NordVPN. The full list is in testing.
Verify the release on your own host with the checks before you rely on it, and keep a way into the host that doesn't depend on the exit. Report vulnerabilities privately; see SECURITY.md.
You pull a signed image and run it with Compose; the commands are in setup.
-
Download
molebridge-<tag>.tar.gzand its.sigstore.jsonfrom the latest release, then verify the bundle, the image and its provenance before you extract or run anything. -
Extract the bundle and copy
.env.exampleto.env. SetOVERLAY_CIDRto your NetBird range and read the other settings: the example has eighteen, each commented. -
Create the tunnel config for your provider with the image's tools, put the NetBird setup key in
secrets/netbird.env, then check and start:docker compose run --rm --no-deps exit check-config docker compose up -d
The bundle pins the image by tag and digest (ghcr.io/irashack/molebridge:v0.8.0@sha256:...),
so the image that runs is the one you verified. A release tag never moves;
:0.8 follows the newest 0.8 patch release. To build from source instead,
see CONTRIBUTING.
flowchart LR
device("Your devices<br/>on NetBird")
provider("Mullvad, PIA or<br/>NordVPN server")
subgraph host["Exit host"]
subgraph exit["exit container · one network namespace"]
peer("NetBird<br/>exit peer")
route("Policy<br/>routing")
tunnel("WireGuard<br/>tunnel")
blocked("Tunnel down:<br/>unreachable")
peer --> route
route --> tunnel
route -.-> blocked
end
panel("switchyard container<br/>the web panel")
end
device --> peer
tunnel --> provider
panel -. "files under ./state" .-> exit
classDef endpoint fill:#24273a,stroke:#8aadf4,color:#cad3f5,stroke-width:2px
classDef routing fill:#24273a,stroke:#a6da95,color:#cad3f5,stroke-width:2px
classDef stopped fill:#24273a,stroke:#ed8796,color:#f4dbd6,stroke-width:2px
classDef panelc fill:#24273a,stroke:#c6a0f6,color:#cad3f5,stroke-width:2px
class device,peer,provider endpoint
class route,tunnel routing
class blocked stopped
class panel panelc
style host fill:#1e2030,stroke:#494d64,color:#cad3f5
style exit fill:#181926,stroke:#494d64,color:#cad3f5
linkStyle default stroke:#8087a2,stroke-width:2px
A native exit is two containers from one image. The exit container owns the
network namespace. Its first process is a supervisor; it starts a worker that
checks the tunnel config, installs the routing rules before the tunnel
exists, brings the tunnel up, runs the provider logic and keeps all of it in
place, and the supervisor starts the pinned NetBird client once the worker
reports ready. The switchyard container is the panel, with no capabilities
and no network in common with the exit. With the gluetun backend, gluetun owns the namespace
and the tunnel, and the exit container joins it and keeps the same routing
rules in place (architecture).
Traffic forwarded from your devices can only go into the tunnel. If the tunnel or its routes disappear, that traffic is dropped, not sent over the host's own connection. The exit's own NetBird and VPN control traffic uses the host's normal route. If the worker stops responding, the supervisor stops NetBird, removes the interfaces it owns, leaves the routing rules in place and exits, and the engine's restart policy starts the container again.
The panel holds no VPN credentials, no gluetun control-server key and no capabilities, and runs no commands. With OpenID Connect sign-in it can hold that sign-in's client secret. It writes one file naming the server you picked. The exit checks that name against the provider's server list, which it downloads itself (or reads from gluetun), then changes the tunnel and reports back.
The panel shows an exit as connected only after the exit has checked the live peer and the address your traffic leaves from. How far that check goes depends on the provider, and the panel says which one an exit got:
- provider-confirmed: the provider itself says the address is theirs. Mullvad, PIA and NordVPN, and Mullvad and NordVPN through gluetun.
- tunnel checks only: a fresh handshake with the selected server, and the address seen through the tunnel differs from the host's own and matches the provider's server list where that list gives exit addresses. No provider confirms it. FastestVPN, IVPN, Surfshark and Windscribe through gluetun.
Details: architecture.
- An always-on Linux host, amd64 or arm64, with kernel WireGuard (Linux 5.6 or later) and Docker Engine with Compose 2.24 or later, or rootless Podman. Windows hosts are not supported. Which of these ran at 0.8.0 is in requirements; only rootless Podman with Quadlet units did.
- Resources: measured on that host with no client traffic, a native Mullvad or PIA exit (NetBird included) idled at 0.3 to 0.5 % of one core and 67 to 78 MB; a NordVPN exit used 189 MB and a gluetun exit 123 MB, because they hold a large server list. The image is 61 MB unpacked (testing). Every byte your devices send crosses the host twice, so the host's upload speed caps throughput.
- A NetBird network, either NetBird Cloud or self-hosted, with admin access to create groups, a policy, a setup key and an exit-node route.
- A VPN account:
- Mullvad, which uses one free device slot;
- or PIA, which needs your username and password on the host;
- or NordVPN (experimental), whose access token is needed only once, at setup;
- or, through the experimental gluetun backend, an account with one of gluetun's providers that Molebridge can select servers for: FastestVPN, IVPN, Mullvad, NordVPN, Surfshark or Windscribe.
- A way to reach the panel with authentication. Unless you set up OpenID Connect sign-in, the panel has no login of its own; see panel access.
No Python is needed on the host. The setup tools run from the same image as throwaway containers (tools reference).
The full list, with the NetBird groups, the outbound ports and the rootless Podman details, is in requirements.
- The panel has no login unless you configure one. By default, anyone who can reach it can change the exit for every device that uses it. Reach it through an SSH tunnel or an authenticating proxy, or turn on OpenID Connect sign-in. Sign-in was tried live with one provider and an admin account only, on 0.7.0's Python panel (testing); the Go panel's has not run against a real identity provider.
- This is not a kill switch on your devices. The exit fails closed for traffic it receives. If a device deselects the exit or NetBird disconnects, that device uses its own connection.
- Molebridge configures no DNS. Devices keep their own resolvers. With Mullvad, plain DNS that passes through the exit ends up at the Mullvad server, so a DNS leak test shows Mullvad; this is observed, not documented by Mullvad. PIA and NordVPN are untested. See DNS.
- Six gluetun providers and three native ones, no others. NordVPN is experimental. The gluetun backend accepts only the providers named above; the exit refuses any other. Other providers says which could be added, and what that support would realistically look like.
- One provider per exit. To offer more than one, run one exit per provider on the same host and serve them from one Switchyard.
- PIA and NordVPN are IPv4 only. PIA's port forwarding is experimental and opens a port to the Internet.
- No automatic failover. Molebridge never picks a different server or region when one fails. It retries the one you chose, with growing pauses; PIA re-registers within the same region when its server stops answering, and with gluetun the exit puts your selection back after gluetun restarts on another server.
- NetBird only. It relies on NetBird's exit-node routes and was never built or tested for Tailscale or other overlays.
- Exits only, with one exception. Molebridge carries traffic for NetBird devices, not for other containers, except in the experimental native sidecar, which exists for providers gluetun can't drive over WireGuard with Molebridge, PIA first. Its routing contract and firewall keep the containers' traffic in the tunnel. To send a container's traffic through any other VPN, use gluetun directly; the experimental gluetun sidecar adds Switchyard to pick and check its server. There, gluetun's firewall alone keeps the containers' traffic in the tunnel, and only Mullvad and NordVPN can be verified.
0.8.0 ports the routing and status logic of 0.7.0, which ran on the same rootless Podman host with a Linux client held through rule and route deletions, tunnel loss, switches and a gluetun restart. Earlier releases ran on macOS with OrbStack and, with podman-compose, on a Debian 13 host. Testing has the dated record of each pass and what has never been tested at any version (Docker Desktop, phones during failure drills, PIA port forwarding against PIA itself, among others).
Install
- Requirements: accounts, host, NetBird groups and ports.
- Setup: from the signed bundle to a working exit.
- Verification: the checks and failure drills to run before you rely on it.
- Panel access: SSH forwarding, an authenticating proxy, or OpenID Connect sign-in.
Use
- Switchyard: switching, embedding in a dashboard, phones, several exits.
- Operations: health, upgrades, recovery, backups, rootless Podman, removal.
- Troubleshooting: symptoms and fixes.
Reference
- Configuration: every setting, file and endpoint.
- Tools: the setup and repair tools that run from the image.
- Providers: how PIA and NordVPN differ from Mullvad, port forwarding, adding a provider.
- Other providers: unsupported VPNs, and what adding one would take.
- gluetun as a NetBird exit: the gluetun form, its routing rules and its checks; experimental.
- gluetun sidecar: Switchyard for an ordinary gluetun that other containers share; experimental.
- Native sidecar: Molebridge's own tunnel, routing contract and firewall for containers that share the exit's namespace, including Kubernetes pods and PIA's forwarded port; experimental, new in 0.9.0.
- Architecture: the processes, routing, trust boundaries and the switching sequence.
- Testing: what has been tested, where and at which revision.
- Roadmap: ideas that aren't scheduled or built.
Contributions are welcome; see CONTRIBUTING.md.
Molebridge isn't affiliated with Mullvad VPN AB, Private Internet Access, NordVPN or NetBird. Their names are used only to say what Molebridge works with. The panel's provider styling uses colors only, with no logos or copied assets. Molebridge is released under the MIT License. The bundled JetBrains Mono font is under the SIL Open Font License, the bundled PIA certificate authority comes with PIA's MIT notice, and the image carries the NetBird client under its own license; THIRD_PARTY.md lists every component.
