Skip to content

Instantly share code, notes, and snippets.

@4piu
Last active January 25, 2026 14:36
Show Gist options
  • Select an option

  • Save 4piu/e08f27ef0032b1a72bdbd063c084929b to your computer and use it in GitHub Desktop.

Select an option

Save 4piu/e08f27ef0032b1a72bdbd063c084929b to your computer and use it in GitHub Desktop.
Route Tailscale exit node traffic to Wireguard

This script enables you to forward all traffic from a Tailscale exit node through a WireGuard tunnel.

Most mobile devices support only one active VPN connection at a time. As a result, users can't simultaneously use Tailscale for connectivity and route their internet traffic through another VPN. This script provides a workaround by turning a Tailscale exit node into a VPN bridge. When clients use this special exit node, their internet traffic is protected by the VPN while still maintaining Tailscale connectivity.

Requirements:

  • Enable forwarding
  • Tailscale need to run with --netfilter-mode=[off|nodivert] for manual routing control
  • Set Table = off in Wireguard config to manual control ip rules
  • Set PostUp and PreDown script in Wireguard config

Example Wireguard Config:

[Interface]
Address = 10.200.200.3/32
PrivateKey = [Client's private key]
DNS = 8.8.8.8
Table = off
PostUp = /usr/local/bin/post-up.sh %i
PreDown = /usr/local/bin/pre-down.sh

[Peer]
PublicKey = [Server's public key]
PresharedKey = [Pre-shared key, same for server and client]
Endpoint = [Server Addr:Server Port]
AllowedIPs = 0.0.0.0/0
#!/usr/bin/env bash
set -e
WG_IFACE=$1
ROUTE_TABLE=39
if [ -z "$WG_IFACE" ]; then
echo "Usage: $0 <wg_interface>"
exit 1
fi
echo "[+] Adding nftables rules..."
nft -f - <<EOF
table inet ts-vpn {
chain prerouting {
type filter hook prerouting priority mangle; policy accept;
iifname "tailscale0" counter packets 0 bytes 0 meta mark set mark and 0xff00ffff or 0x0040000
}
chain input {
type filter hook input priority filter; policy accept;
iifname != "tailscale0" ip saddr 100.115.92.0/23 counter packets 0 bytes 0 return
iifname != "tailscale0" ip saddr 100.64.0.0/10 counter packets 0 bytes 0 drop
iifname "tailscale0" counter packets 0 bytes 0 accept
}
chain forward {
type filter hook forward priority filter; policy accept;
oifname "tailscale0" ip saddr 100.64.0.0/10 counter packets 0 bytes 0 drop
}
chain postrouting {
type nat hook postrouting priority srcnat; policy accept;
meta mark & 0x00ff0000 == 0x00040000 counter packets 0 bytes 0 masquerade
}
}
EOF
echo "[+] Adding routing rule for marked packets..."
ip route add default dev "$WG_IFACE" table $ROUTE_TABLE || true
ip -6 route add default dev "$WG_IFACE" table $ROUTE_TABLE || true
ip rule add fwmark 0x40000/0xff0000 lookup $ROUTE_TABLE || true
ip -6 rule add fwmark 0x40000/0xff0000 lookup $ROUTE_TABLE || true
#!/usr/bin/env bash
set -e
WG_IFACE=$1
ROUTE_TABLE=39
if [ -z "$WG_IFACE" ]; then
echo "Usage: $0 <wg_interface>"
exit 1
fi
echo "[-] Deleting nftables rules..."
nft delete table inet ts-vpn || true
echo "[-] Removing routing rules..."
ip rule del fwmark 0x40000/0xff0000 lookup $ROUTE_TABLE || true
ip -6 rule del fwmark 0x40000/0xff0000 lookup $ROUTE_TABLE || true
ip route flush table $ROUTE_TABLE || true
ip -6 route flush table $ROUTE_TABLE || true
@DarthKilroy
Copy link

Thank you so much for this! I spent so long trying to figure this out.

@CristianKerr
Copy link

CristianKerr commented Jan 24, 2026

Hi, thanks a lot for this, I have been trying to set this up whole day.
One additional question: what do I need to add in order to advertise routes of the local network? With tailscale I would use e.g. "--advertise-routes=192.168.1.0/24"
Thanks a lot!

@4piu
Copy link
Author

4piu commented Jan 25, 2026

Hi, thanks a lot for this, I have been trying to set this up whole day. One additional question: what do I need to add in order to advertise routes of the local network? With tailscale I would use e.g. "--advertise-routes=192.168.1.0/24" Thanks a lot!

Here's what might work if you want the same tailscale instance also advertise the subnet-router, but I haven't tested:

table inet ts-warp {
    chain prerouting {
        type filter hook prerouting priority mangle; policy accept;

        # ─── NEW: do NOT mark traffic destined for your own advertised LAN subnets ───
        iifname "tailscale0" ip daddr 192.168.1.0/24 counter meta mark set 0x0 return
        # repeat for every subnet you advertise, e.g.:
        # iifname "tailscale0" ip daddr fd12:3456::/64   counter meta mark set 0x0 return  (for IPv6 subnets)

        # existing mark rule (only reaches here if daddr did NOT match the subnets above)
        iifname "tailscale0" counter meta mark set mark and 0xff00ffff or 0x0040000
    }
    ...
}

In my use case, I run a stand alone tailscale instance that serves only as the subnet router. TS client access subnets without issue when using exit node.

@CristianKerr
Copy link

CristianKerr commented Jan 25, 2026

@4piu thank you, I tried, it doesn't work. The counter goes up though. So maybe issue with the packages not finding the way back? I am not sure I am very new to nftables.
Okay before I finished writing here I tried to add masquerade to the postrouting and it works now!:
iifname "tailscale0" ip daddr 192.168.1.0/24 counter packets 0 bytes 0 masquerade
This is something I was trying yesterday but it didn't work on itself.
Thanks a lot!

I run a stand alone tailscale instance that serves only as the subnet router

Aha so you don't use this configuration anymore? Was it unstable? Just trying to figure out if I can use this long term 😊

@4piu
Copy link
Author

4piu commented Jan 25, 2026

Glad you made it work, your reply could help other people in the future.

Aha so you don't use this configuration anymore? Was it unstable? Just trying to figure out if I can use this long term 😊

To calrify, I still use this to run an exit node that bridges to Cloudflare Warp, which runs in my VPS. For subnet router to access my home and office, I run seperate TS instance and advertise router accordingly.

I haven't had stability issue with this setup so far.

@CristianKerr
Copy link

Aha, got it, thank you 😊

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment