Use Mullvad & Tailscale together
  • Go 35.2%
  • Shell 32.5%
  • JavaScript 14.8%
  • CSS 9.6%
  • HTML 2.9%
  • Other 5%
Find a file
2026-03-23 17:29:30 +05:30
static feat: add mullvad-tailscale 2026-03-23 17:29:30 +05:30
Dockerfile feat: add mullvad-tailscale 2026-03-23 17:29:30 +05:30
entrypoint.sh feat: add mullvad-tailscale 2026-03-23 17:29:30 +05:30
go.mod feat: add mullvad-tailscale 2026-03-23 17:29:30 +05:30
handlers.go feat: add mullvad-tailscale 2026-03-23 17:29:30 +05:30
justfile feat: add mullvad-tailscale 2026-03-23 17:29:30 +05:30
LICENSE Initial commit 2026-03-23 11:58:12 +00:00
main.go feat: add mullvad-tailscale 2026-03-23 17:29:30 +05:30
mullvad-switcher.service feat: add mullvad-tailscale 2026-03-23 17:29:30 +05:30
mullvad.go feat: add mullvad-tailscale 2026-03-23 17:29:30 +05:30
README.md feat: add mullvad-tailscale 2026-03-23 17:29:30 +05:30
routing.sh feat: add mullvad-tailscale 2026-03-23 17:29:30 +05:30
setup.sh feat: add mullvad-tailscale 2026-03-23 17:29:30 +05:30
supervisord.conf feat: add mullvad-tailscale 2026-03-23 17:29:30 +05:30
tailscale-up.sh feat: add mullvad-tailscale 2026-03-23 17:29:30 +05:30
wireguard.go feat: add mullvad-tailscale 2026-03-23 17:29:30 +05:30

Mullvad-Tailscale Exit Node

A Docker container that turns a Mullvad WireGuard VPN into a Tailscale exit node, with a web UI to switch between Mullvad server locations.

Any device on your tailnet can use this as an exit node to route all traffic through Mullvad.

How it works

Tailscale clients → tailscale0 → wg0 → Mullvad → Internet

The container runs in its own network namespace with three processes managed by supervisord:

  1. tailscaled — creates the tailscale0 interface, advertises as an exit node
  2. mullvad-switcher — web UI for switching Mullvad servers
  3. WireGuardwg0 tunnel to Mullvad

Policy-based routing (fwmark) steers traffic arriving on tailscale0 through wg0, while the container's own control-plane traffic (Tailscale coordination, API calls) uses the default Docker network.

Prerequisites

  • Docker with NET_ADMIN and SYS_MODULE capabilities
  • A Mullvad account with a WireGuard config (wg0.conf)
  • A Tailscale account with an auth key (or interactive login)

Getting a Mullvad WireGuard config

  1. Go to mullvad.net/en/account/wireguard-config
  2. Generate a key and download a config file
  3. Remove the DNS line from the config (the container uses Docker's DNS)
  4. Add PersistentKeepalive = 25 to the [Peer] section
  5. Ensure Table = off is in the [Interface] section (routing is handled by the container)

Your wg0.conf should look like this:

[Interface]
MTU = 1420
Table = off
PrivateKey = <your-private-key>
Address = <your-address>/32

[Peer]
PublicKey = <server-public-key>
AllowedIPs = 0.0.0.0/0,::0/0
Endpoint = <server-ip>:<port>
PersistentKeepalive = 25

Quick start

# docker-compose.yml
services:
  mullvad-tailscale:
    image: mullvad-tailscale:latest
    container_name: mullvad-tailscale
    cap_add:
      - NET_ADMIN
      - SYS_MODULE
    devices:
      - /dev/net/tun:/dev/net/tun
    sysctls:
      - net.ipv4.ip_forward=1
      - net.ipv6.conf.all.forwarding=1
    volumes:
      - tailscale-state:/var/lib/tailscale
      - ./wg0.conf:/etc/wireguard/wg0.conf
    environment:
      - TS_AUTHKEY=tskey-auth-xxxxx  # from https://login.tailscale.com/admin/settings/keys
      - TS_HOSTNAME=mullvad-exit     # optional, sets the Tailscale hostname
    restart: unless-stopped

volumes:
  tailscale-state:
# Place your wg0.conf next to docker-compose.yml, then:
docker compose up -d

The container will:

  1. Bring up the WireGuard tunnel to Mullvad
  2. Set up routing rules
  3. Start Tailscale and authenticate with your auth key
  4. Advertise itself as an exit node

Without an auth key

If you don't provide TS_AUTHKEY, you'll need to authenticate interactively:

docker compose up -d
docker exec -it mullvad-tailscale tailscale up --advertise-exit-node
# Follow the printed URL to authenticate

Approve the exit node

After authentication, approve the exit node in the Tailscale admin console — click the machine, then enable "Use as exit node".

Building the image

# Build for arm64 (default)
docker build --platform linux/arm64 --load -t mullvad-tailscale:latest .

# Build for amd64
docker build --platform linux/amd64 --load -t mullvad-tailscale:latest .

Web UI

Once running, the switcher web UI is available at http://<tailscale-ip>:8080 from any device on your tailnet.

From the UI you can:

  • See the current Mullvad server and connection status
  • Browse available servers by country and city
  • Switch to a different server with one tap

Environment variables

Variable Default Description
TS_AUTHKEY (empty) Tailscale auth key for headless setup
TS_HOSTNAME (empty) Tailscale machine hostname

Architecture

┌─────────────────────────────────────────────┐
│  Container (isolated network namespace)     │
│                                             │
│  ┌───────────┐  ┌────────┐  ┌───────────┐  │
│  │ tailscaled│  │ mullvad │  │ WireGuard │  │
│  │           │  │-switcher│  │  (wg0)    │  │
│  └─────┬─────┘  └────┬───┘  └─────┬─────┘  │
│        │              │            │        │
│    tailscale0     :8080 web    Mullvad VPN  │
│        │                           │        │
│        └──── fwmark routing ───────┘        │
│                                             │
│  eth0 (Docker bridge, control plane only)   │
└─────────────────────────────────────────────┘

License

MIT