====== OpenWrt Tailscale Notes ======
===== Tailscale Exit Node with NAT Hairpin (Loopback) =====
Prerequisite: [[https://openwrt.org/docs/guide-user/services/vpn/tailscale/start|OpenWrt Tailscale guide]] done (zone, masq, forwarding, exit node).
All steps via SSH.
- Create ''/etc/firewall.tailscale-reflect.sh'':
#!/bin/sh
nft insert rule inet fw4 dstnat iifname tailscale0 jump dstnat_tailscale 2>/dev/null
. /lib/functions/network.sh
network_get_ipaddr WANIP wan
[ -n "$WANIP" ] || exit 0
i=0
while uci -q get firewall.@redirect[$i] >/dev/null 2>&1; do
if [ "$(uci -q get firewall.@redirect[$i].src)" = "wan" ] \
&& [ "$(uci -q get firewall.@redirect[$i].target)" = "DNAT" ]; then
dest_ip=$(uci -q get firewall.@redirect[$i].dest_ip)
dport=$(uci -q get firewall.@redirect[$i].src_dport)
dest_port=$(uci -q get firewall.@redirect[$i].dest_port)
[ -n "$dest_port" ] || dest_port="$dport"
protos=$(uci -q get firewall.@redirect[$i].proto)
[ -n "$protos" ] || protos="tcp"
for proto in $protos; do
nft add rule inet fw4 dstnat_tailscale \
ip daddr "$WANIP" "$proto" dport "$dport" \
dnat ip to "${dest_ip}:${dest_port}" \
comment "tailscale-reflect-auto" 2>/dev/null
nft add rule inet fw4 srcnat_lan \
ip daddr "$dest_ip" "$proto" dport "$dest_port" \
snat ip to "$WANIP" \
comment "tailscale-reflect-auto" 2>/dev/null
done
fi
i=$((i + 1))
done
- Make it executable:
chmod 755 /etc/firewall.tailscale-reflect.sh
- Register as firewall include:
uci add firewall include
uci set firewall.@include[-1].type='script'
uci set firewall.@include[-1].path='/etc/firewall.tailscale-reflect.sh'
uci set firewall.@include[-1].fw4_compatible='1'
uci commit firewall
- Apply:
fw4 reload
- Verify:
nft list chain inet fw4 dstnat | grep tailscale
nft list ruleset | grep -A6 'chain dstnat_tailscale'
- Test from a device using this router as Tailscale exit node.
==== Notes ====
* Adding/removing a ''wan''-sourced port forward auto-updates on next reload -- nothing else to do.
* Self-heals on WAN IP change (reload already triggers via stock ''/etc/hotplug.d/iface/20-firewall'').
* Don't use "Enable NAT Loopback" / ''reflection_zone'' for the ''tailscale'' zone -- fails silently, fw4 can't detect the interface's subnet.
* fw4 only auto-wires the ''dstnat'' dispatch rule for a zone if a UCI redirect uses ''src'' = that zone. Since none do here, the script inserts it manually every reload -- required, not optional.