Skip to content

Instantly share code, notes, and snippets.

@federkamm
Created January 8, 2026 07:22
Show Gist options
  • Select an option

  • Save federkamm/b85b0a8acaca9d086ac6616e0219ca16 to your computer and use it in GitHub Desktop.

Select an option

Save federkamm/b85b0a8acaca9d086ac6616e0219ca16 to your computer and use it in GitHub Desktop.
nft: split tunnel VPN by user-group (with firewall, custom DNS, and kill-switch)
#!/usr/bin/env bash
# - untrusted vpn on tun0
# - don't accept unexpected input (chain vpn/input)
# - don't forward any traffic (chain vpn/forward)
# - route traffic from group 'vpn' over tun0, (e.g. "sg vpn 'curl ifconfig.me'")
# - NOTE: chain vpn/route needs to be of type 'route' to trigger route lookup by fwmark!
# - nat DNS traffic (port 53) to custom DNS (chain vpn/nat)
# - masquerade traffic over tun0 since initial route lookup suggests bindings to wrong socket!
# - kill marked traffic that does not travel over tun0
ip route add default via 10.0.0.1 dev tun0 table 1
ip rule add fwmark 1 lookup 1
nft -f - <<EOF
table inet vpn
flush table inet vpn
delete table inet vpn
table inet vpn {
chain input {
type filter hook input priority filter;
iif "tun0" ct state != { established, related } drop
}
chain forward {
type filter hook forward priority filter;
iif "tun0" drop
}
chain route {
type route hook output priority mangle;
meta skgid vpn ct mark set 1
ct mark 1 meta mark set 1
}
chain nat {
type nat hook output priority -100; # dstnat
th dport 53 ct mark 1 dnat ip to 9.9.9.9
}
chain masq {
type nat hook postrouting priority srcnat;
ct mark 1 masquerade
}
chain kill {
type filter hook postrouting priority security;
meta mark 1 oif != "tun0" drop
}
}
EOF
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment