pluja/tailwg-exit
1
0
Fork
You've already forked tailwg-exit
0
Containerized Tailscale exit node that forwards exit-node traffic through a commercial WireGuard VPN provider.
  • Shell 93.2%
  • Dockerfile 6.8%
2026年05月05日 11:07:58 +03:00
config initial commit 2026年04月27日 09:26:24 +03:00
scripts add interactive setup script 2026年05月05日 11:07:58 +03:00
.dockerignore initial commit 2026年04月27日 09:26:24 +03:00
.env.example improve exit node performance 2026年05月05日 10:52:49 +03:00
.gitignore initial commit 2026年04月27日 09:26:24 +03:00
docker-compose.yml improve exit node performance 2026年05月05日 10:52:49 +03:00
Dockerfile initial commit 2026年04月27日 09:26:24 +03:00
README.md add interactive setup script 2026年05月05日 11:07:58 +03:00

tailwg-exit

Containerized Tailscale exit node that forwards internet traffic through a commercial WireGuard VPN provider.

Traffic flow:

User device -> Tailscale -> tailwg-exit -> provider WireGuard VPN -> Internet

Tailnet traffic still goes directly over Tailscale:

User device -> Tailscale -> destination tailnet device

This project is designed to be small, auditable, and fail-closed.

What It Does

  • Runs Tailscale inside a container.
  • Advertises that container as a Tailscale exit node.
  • Runs a second WireGuard tunnel to your VPN provider.
  • Policy-routes only Tailscale exit-node client traffic into the provider VPN.
  • NATs that forwarded traffic out the provider tunnel.
  • Rejects forwarded traffic if it would leave anywhere except the provider tunnel.

This avoids Android's one-VPN limitation because your Android device only runs Tailscale locally.

Security Model

This setup improves privacy, but it is not magic anonymity.

What it helps with:

  • Your phone/laptop uses only Tailscale locally.
  • Internet egress comes from your VPN provider, not your home IP.
  • Exit-node traffic should not leak through your host's normal interface if wg0 goes down.
  • Tailscale ACLs can prevent the exit-node container from initiating access to the rest of your tailnet.

What it does not hide:

  • Tailscale still sees coordination metadata.
  • Your ISP sees your server connecting to the VPN provider.
  • Your VPN provider sees internet egress metadata.
  • HTTPS is still required to protect website contents.

Project Files

Dockerfile Debian image with Tailscale and WireGuard tools
docker-compose.yml Container definition
.env.example Environment template
config/wg0.conf.example Provider WireGuard template
scripts/entrypoint.sh Startup, routing, NAT, and leak protection
.dockerignore Keeps secrets and state out of builds
.gitignore Ignores secrets, runtime state, and editor files

Published image:

codeberg.org/pluja/tailwg-exit:latest

Use an always-on Linux machine.

Minimum:

1 CPU core
512 MB RAM
1 GB disk
Docker + Compose

Recommended:

Small Debian/Ubuntu VM or LXC
1-2 CPU cores
1 GB RAM
Docker installed

Using a dedicated VM or LXC is more production-grade than running this on a busy general-purpose Docker host, because the container needs /dev/net/tun, NET_ADMIN, and NET_RAW.

Quick Start

Interactive installer:

curl -fsSL https://codeberg.org/pluja/tailwg-exit/raw/branch/main/scripts/setup.sh | bash

The installer will:

  • Create ~/tailwg-exit by default.
  • Download docker-compose.yml and example config files.
  • Ask for your Tailscale auth key, hostname, and tag.
  • Import or paste your provider WireGuard config.
  • Add Table = off if missing.
  • Comment out DNS = ... in the provider config to avoid resolver surprises.
  • Pull codeberg.org/pluja/tailwg-exit:latest.
  • Optionally start the container.

Manual setup:

  1. Create a Tailscale tag named exit-node.
  2. Create a tagged Tailscale auth key for tag:exit-node.
  3. Put your provider WireGuard config in config/wg0.conf.
  4. Start the container.
  5. Approve the machine and approve the exit node route in Tailscale admin.
  6. Select tailwg-exit as exit node from your clients.

The detailed manual steps are below.

1. Prepare The Project

From the project directory:

cp .env.example .env
cp config/wg0.conf.example config/wg0.conf
mkdir -p state/tailscale
chmod 700 state
chmod 700 state/tailscale
chmod 600 config/wg0.conf

Edit .env:

TS_AUTHKEY=tskey-auth-REPLACE_ME
TS_HOSTNAME=tailwg-exit
TS_ADVERTISE_TAGS=tag:exit-node
TS_ACCEPT_DNS=false
TS_NETFILTER_MODE=off
WG_MTU=
TCP_MSS_CLAMP=auto

Important variables:

  • TS_AUTHKEY: only needed for first login, or if you delete state/tailscale
  • TS_HOSTNAME: how the node appears in Tailscale
  • TS_ADVERTISE_TAGS: should be tag:exit-node
  • TS_ACCEPT_DNS=false: prevents the container from accepting Tailscale DNS changes
  • TS_NETFILTER_MODE=off: lets this project own the forwarding/NAT rules instead of mixing in Tailscale netfilter rules
  • WG_MTU=: uses the MTU from your provider WireGuard config; set only if you need an override
  • TCP_MSS_CLAMP=auto: clamps TCP SYN packets to path MTU to avoid upload/download stalls without forcing a tiny fixed MSS

Keep this default value unchanged:

TAILSCALE_IPV4_CIDR=100.64.0.0/10

That CIDR covers the full Tailscale IPv4 range. Do not replace it with your specific device IPs.

2. Create The Tailscale Tag

In the Tailscale admin console:

  1. Go to Access Controls.
  2. Create tag exit-node.
  3. Set the tag owner to autogroup:admin.

Meaning:

  • The real tag name becomes tag:exit-node.
  • Only admins can assign it.

3. Configure ACLs

Use a policy that lets members use exit nodes but does not let tag:exit-node initiate connections into your tailnet.

Example working ACL file:

{
 "tagOwners": {
 "tag:router": ["autogroup:admin"],
 "tag:exit-node": ["autogroup:admin"]
 },
 "grants": [
 {
 "src": ["autogroup:member"],
 "dst": ["autogroup:internet"],
 "ip": ["*"]
 }
 ],
 "acls": [
 {
 "action": "accept",
 "src": ["autogroup:member"],
 "dst": ["autogroup:member:*", "tag:router:*", "tag:exit-node:*"]
 },
 {
 "action": "accept",
 "src": ["tag:router"],
 "dst": ["*:*"]
 }
 ],
 "ssh": [
 {
 "action": "check",
 "src": ["autogroup:member"],
 "dst": ["autogroup:self"],
 "users": ["autogroup:nonroot", "root"]
 }
 ]
}

Simple summary:

  • Members can talk to other members.
  • Members can talk to routers.
  • Members can see/use the exit node.
  • Members can use autogroup:internet, which is required for exit nodes.
  • Routers can still reach everything.
  • The exit node cannot initiate connections to your devices because there is no rule with src: ["tag:exit-node"].

Do not keep a broad allow-all rule like this if you want isolation:

{
 "action": "accept",
 "src": ["*"],
 "dst": ["*:*"]
}

4. Create The Auth Key

In the Tailscale admin console, use Add Linux server only to generate the auth key. Do not run the generated install command on the host.

Recommended settings:

  • Tags: On
  • Tag: tag:exit-node
  • Ephemeral: Off
  • Use as exit node: On
  • Reusable: Off
  • Short expiration: fine, as long as you use it soon

Tailscale may generate a command like:

curl -fsSL https://tailscale.com/install.sh | sh && sudo tailscale up --auth-key=tskey-auth-... --advertise-exit-node

Do not run that command for this project.

Only copy the auth key value:

tskey-auth-...

and place it in .env:

TS_AUTHKEY=tskey-auth-...

5. Add Your Provider WireGuard Config

Put your provider config at:

config/wg0.conf

For this project, modify it to keep only IPv4 first.

Required changes:

  • Add Table = off under [Interface]
  • Remove DNS = ...
  • Keep only the IPv4 address initially
  • Keep only AllowedIPs = 0.0.0.0/0 initially

Example:

[Interface]
PrivateKey = REPLACE_ME
Address = 10.0.0.2/32
Table = off
[Peer]
PublicKey = REPLACE_ME
Endpoint = vpn-provider.example:51820
AllowedIPs = 0.0.0.0/0
PersistentKeepalive = 25

Why:

  • Table = off prevents wg-quick from hijacking the whole routing table.
  • Removing DNS = ... avoids resolver surprises and metadata leaks.
  • IPv4-only first is safer. Broken IPv6 is a common leak source.

If your provider gave you IPv6 too, ignore it at first. Add it later only after IPv4 works and is leak-tested.

6. Start The Container

Option 1: build locally

docker compose up -d --build

Option 2: use the published image

docker compose pull
docker compose up -d

Watch logs:

docker compose logs -f

Expected shape:

starting tailscaled
starting WireGuard provider tunnel
installing policy routes
installing IPv4 firewall and leak protection
ready: use tailwg-exit as a Tailscale exit node

Harmless warnings you may see:

  • TPM: error opening /dev/tpmrm0
  • Warning: IPv6 forwarding is disabled
  • UDP GRO forwarding is suboptimally configured

These are not blockers for the IPv4-only setup. If you want maximum throughput later, tune the Docker host for UDP offload/GRO according to Tailscale's current Linux optimization docs.

7. Approve The Machine And The Exit Node

This is the step that is easiest to miss.

After startup, the node may appear in Tailscale admin but still not be usable as an exit node.

You must do both:

  1. Approve the new machine.
  2. Approve the exit-node route.

If the dashboard shows Waiting approval, approve it.

If the machine appears in your tailnet but tailscale exit-node list says no exit nodes found, the exit-node route is probably still waiting approval.

After approval, clients should see it as an exit node.

8. Verify On The Server

Run:

docker exec tailwg-exit tailscale status
docker exec tailwg-exit wg show wg0
docker exec tailwg-exit ip rule
docker exec tailwg-exit ip route show table providerwg
docker exec tailwg-exit iptables -S FORWARD
docker exec tailwg-exit iptables -t nat -S POSTROUTING

Healthy signs:

  • wg show wg0 has a recent handshake
  • ip rule contains:
from 100.64.0.0/10 lookup providerwg
  • ip route show table providerwg contains:
default dev wg0 scope link
  • iptables -S FORWARD contains:
-A FORWARD -i tailscale0 -o wg0 -j ACCEPT
-A FORWARD -i wg0 -o tailscale0 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
-A FORWARD -i tailscale0 ! -o wg0 -j REJECT
  • iptables -t nat -S POSTROUTING contains:
-A POSTROUTING -s 100.64.0.0/10 -o wg0 -j MASQUERADE
  • iptables -t mangle -S FORWARD contains TCPMSS rules using either --clamp-mss-to-pmtu or your configured fixed MSS.

That reject rule is the key leak-protection rule. If wg0 dies, forwarded client traffic should not fall back to your normal network.

9. Verify From Another Tailscale Client

On Linux:

tailscale exit-node list

You should see:

tailwg-exit.dojo-arctic.ts.net

To connect to it:

sudo tailscale set --exit-node=tailwg-exit --exit-node-allow-lan-access=true

To disconnect later:

sudo tailscale set --exit-node=

On Android:

Open Tailscale -> Exit node -> select tailwg-exit

If it does not appear in Android:

  • Make sure the exit-node route was approved in Tailscale admin.
  • Make sure your ACL includes tag:exit-node:* in the member destination list.
  • Disconnect and reconnect the Android app.

10. Verify Public IP And Tailnet Reachability

While using the exit node, check your public IP:

curl https://ifconfig.me

Expected result:

  • It should show your provider VPN egress IP.
  • It should not show your home/public server IP.

Then test tailnet reachability too:

ping 100.x.y.z

That should still work over Tailscale.

For transparency under real traffic, also test a large download and upload while using the exit node:

curl -L -o /tmp/test.bin https://speed.cloudflare.com/__down?bytes=100000000
curl -T /tmp/test.bin https://speed.cloudflare.com/__up

Both should complete without hanging. Hangs during large transfers usually mean an MTU/PMTU problem; keep TCP_MSS_CLAMP=auto enabled, and only set a fixed lower WG_MTU or TCP_MSS_CLAMP if your provider path is broken.

This is the desired behavior:

Internet -> provider VPN
Tailnet traffic -> direct Tailscale

11. Remove The Auth Key After First Successful Login

Once the node is registered and state/tailscale exists, remove the auth key from .env:

TS_AUTHKEY=

or delete the line entirely.

Then restart once to verify persisted state works:

docker compose restart
docker compose logs -f

If it comes back and reaches:

ready: use tailwg-exit as a Tailscale exit node

then the persistent state is working and you no longer need the auth key on disk.

12. DNS Privacy Notes

DNS is a common metadata leak.

Recommended approach:

  • Do not put DNS = ... in config/wg0.conf initially.
  • Keep TS_ACCEPT_DNS=false in the container.
  • Let your clients use Tailscale DNS settings or OS-level encrypted DNS.
  • If using MagicDNS, remember that tailnet name lookups involve Tailscale DNS infrastructure.

Simple advice: get the routing working first, then tune DNS only if you have a specific privacy goal and understand the tradeoffs.

13. IPv6

IPv6 is disabled by default in this project.

Keep it disabled until IPv4 works and you have tested for leaks.

To enable IPv6 later:

  1. Add the IPv6 address back into config/wg0.conf.
  2. Change AllowedIPs to:
AllowedIPs = 0.0.0.0/0, ::/0
  1. Set:
ENABLE_IPV6=true
  1. Restart and run IPv6 leak tests.

If you are unsure, stay IPv4-only.

14. ACL Isolation Test

You want clients to use the exit node, but you do not want the exit node to be able to browse your tailnet.

Test from inside the container:

docker exec tailwg-exit ping 100.x.y.z

If your ACLs are correct, that should fail unless you explicitly allowed that path.

This asymmetry is intentional:

Clients -> exit node: allowed
Exit node -> clients/internal tailnet devices: denied

15. Hardening Checklist

Do this:

  • Run on a dedicated VM/LXC or dedicated Docker host if possible.
  • Keep the host updated.
  • Use SSH keys only on the host.
  • Keep config/wg0.conf mode 0600.
  • Remove TS_AUTHKEY after first successful registration.
  • Keep IPv6 disabled until verified.
  • Keep TCP_MSS_CLAMP=auto unless you have measured a better provider-specific value.
  • Use a WireGuard config dedicated to this gateway.
  • Use Tailscale ACLs to block tag:exit-node from initiating access to other tailnet devices.
  • Do not advertise subnet routes from this container.
  • Do not publish ports from this container.

Avoid this:

  • Do not use broad allow-all ACLs.
  • Do not run unrelated services in this container.
  • Do not mount the Docker socket.
  • Do not run with privileged: true unless absolutely necessary.
  • Do not use host networking unless you fully understand the routing impact.

16. Troubleshooting

The machine appears in Tailscale, but not as an exit node

Check the admin dashboard. The exit-node route is probably still waiting approval.

tailscale exit-node list says no exit nodes found

One of these is usually true:

  • Exit-node route not approved yet
  • ACL does not allow autogroup:internet
  • ACL does not allow members to reach tag:exit-node:*

Android does not show the exit node

Check:

  • exit-node route approval
  • ACL includes tag:exit-node:*
  • app disconnect/reconnect

wg show wg0 has no handshake

Check:

  • provider endpoint
  • provider public key
  • private key
  • host outbound UDP filtering
  • server clock/time

Public IP is your home/server IP instead of the provider IP

Stop using the exit node immediately and inspect:

  • ip rule
  • ip route show table providerwg
  • iptables -S FORWARD
  • iptables -t nat -S POSTROUTING

Expected route table:

from 100.64.0.0/10 lookup providerwg
default dev wg0 scope link

Production Note

This project is intentionally simple. The smaller the networking stack, the easier it is to audit. For the strongest operational setup, run it in a small dedicated VM or LXC and snapshot the working configuration.