Created
July 14, 2026 06:57
-
-
Save NicholasClooney/fb3fb65955b42fafd1da8e632d75678c to your computer and use it in GitHub Desktop.
Companion snippets: GL.iNet Slate 7, Tailscale exit node, and China split-routing (blog.clooney.io)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # Before: Tailscale's exit-node rule wins before PBR marks are evaluated. | |
| # Any packet -- even ones GL.iNet/PBR meant to send direct -- is grabbed | |
| # by table 52 and sent out tailscale0. | |
| 5270: from all lookup 52 # Tailscale exit node | |
| 6000: from all fwmark 0x8000/0xf000 lookup main | |
| 30000: from all fwmark 0x10000/0xff0000 lookup pbr_wan | |
| 30001: from all fwmark 0x20000/0xff0000 lookup pbr_secondwan | |
| 30002: from all fwmark 0x30000/0xff0000 lookup pbr_wwan | |
| # After: early PBR rules (5261-5263) run before Tailscale's 5270. | |
| # China-marked packets take pbr_wan/pbr_wwan and go out the physical uplink; | |
| # everything else falls through to 5270 and rides the exit node. | |
| 5261: from all fwmark 0x10000/0xff0000 lookup pbr_wan | |
| 5262: from all fwmark 0x20000/0xff0000 lookup pbr_secondwan | |
| 5263: from all fwmark 0x30000/0xff0000 lookup pbr_wwan | |
| 5270: from all lookup 52 # Tailscale exit node |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/bin/sh | |
| # Router-level Tailscale exit-node control on the Slate 7. | |
| # | |
| # --exit-node-allow-lan-access: LAN clients (192.168.8.0/24) can still | |
| # reach the router and each other while their default route is the | |
| # exit node. | |
| # --accept-dns=false: do NOT let Tailscale rewrite the router's DNS. | |
| # dnsmasq stays authoritative so the China split-DNS keeps working. | |
| # Enable | |
| tailscale set \ | |
| --exit-node=debian \ | |
| --exit-node-allow-lan-access \ | |
| --accept-dns=false | |
| # Disable | |
| tailscale set --exit-node= --accept-dns=false | |
| # Safety net while testing remotely: auto-disable after 3 minutes | |
| # in case the exit node breaks routing and takes SSH with it. | |
| sleep 180 && tailscale set --exit-node= --accept-dns=false & |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/bin/sh | |
| # Make LAN clients able to egress through the router's Tailscale exit node. | |
| # Two things are needed: | |
| # 1. A forwarding rule allowing lan -> tailscale0. | |
| # 2. MASQUERADE on the tailscale0 zone so return packets come back | |
| # to the router's tailnet IP, not the LAN client's 192.168.8.x. | |
| # 1. Permanent forwarding entry lan -> tailscale0 | |
| uci add firewall forwarding | |
| uci set firewall.@forwarding[-1].src='lan' | |
| uci set firewall.@forwarding[-1].dest='tailscale0' | |
| uci set firewall.@forwarding[-1].name='lan_tailscale' | |
| uci commit firewall | |
| # 2. tailscale0 zone with MASQUERADE | |
| # (usually already present from the GL.iNet tailscale package; shown for reference) | |
| # | |
| # firewall.tailscale0.name='tailscale0' | |
| # firewall.tailscale0.device='tailscale0' | |
| # firewall.tailscale0.input='ACCEPT' | |
| # firewall.tailscale0.output='ACCEPT' | |
| # firewall.tailscale0.forward='REJECT' | |
| # firewall.@nat[0].name='Tailscale NAT' | |
| # firewall.@nat[0].src='tailscale0' | |
| # firewall.@nat[0].target='MASQUERADE' | |
| /etc/init.d/firewall reload |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # GL.iNet's vpn_table stamps every unmarked LAN packet with 0x8000. | |
| # This is what caused the "LAN clients can't reach Google/YouTube even | |
| # though the router itself works fine" bug. | |
| # | |
| # Router-origin traffic goes through LOCAL_POLICY (not shown), which | |
| # only marks packets from skgid 10000 -- that is why the router itself | |
| # was unaffected and reached the exit node normally. | |
| table inet vpn_table { | |
| chain PREROUTING { | |
| iifname "br-*" jump ROUTE_POLICY | |
| } | |
| chain ROUTE_POLICY { | |
| jump TUNNEL100_ROUTE_POLICY | |
| } | |
| chain TUNNEL100_ROUTE_POLICY { | |
| # If the low nibble of fwmark bits 0x0000f000 is zero, OR in 0x8000. | |
| # Result: every unmarked LAN packet gets 0x8000. | |
| meta mark & 0x0000f000 == 0x00000000 \ | |
| meta mark set meta mark & 0xffff8fff | 0x00008000 | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # The symptom: LAN client 192.168.8.128 tries to reach a Google IP. | |
| # The reply direction shows dst=192.168.0.109, which is the router's | |
| # sta0 (physical uplink) IP, not 100.106.152.123 (its tailnet IP). | |
| # | |
| # Translation: the LAN packet was MASQUERADEd out sta0 by the physical | |
| # uplink instead of tailscale0. The Chinese ISP then either dropped or | |
| # reset the return, and the TCP handshake never completed. | |
| tcp SYN_SENT src=192.168.8.128 dst=142.251.218.97 sport=62497 dport=443 | |
| [UNREPLIED] src=142.251.218.97 dst=192.168.0.109 sport=443 dport=62497 | |
| # Root cause: | |
| # - GL.iNet's vpn_table stamps all LAN packets with 0x8000. | |
| # - An early IP rule installed by pbr.user.china (pref 5260) matched | |
| # fwmark 0x8000/0xf000 and pointed at `main`, which is the physical | |
| # uplink's default route. | |
| # - So every LAN packet was hijacked to sta0 *before* Tailscale's | |
| # pref 5270 got a chance to route it via tailscale0. | |
| # | |
| # Fix: stop installing pref 5260. The China marks (0x10000/0x20000/0x30000) | |
| # handled by 5261/5262/5263 are enough; the 0x8000 catch-all did more | |
| # harm than good. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # /etc/dnsmasq.d/accelerated-domains.china.conf (excerpt) | |
| # | |
| # Concept: | |
| # - CN domains (from felixonmars/dnsmasq-china-list, ~111k suffixes) | |
| # resolve via AliDNS 223.5.5.5, which is reached DIRECTLY through | |
| # the physical uplink. Chinese CDNs and geo-fenced services return | |
| # nearby POP IPs this way, so Taobao/WeChat/banking stay fast. | |
| # | |
| # - Everything else resolves via stubby DoT (127.0.0.1#5453) which | |
| # forwards to Cloudflare 1.1.1.1. Stubby's traffic rides the | |
| # Tailscale exit node, bypassing ISP DNS poisoning of foreign names. | |
| # | |
| # 223.5.5.5 itself sits inside the China CIDR set, so the PBR mark | |
| # 0x30000 is applied to it at output and no extra rule is needed. | |
| # CN suffix list (excerpt of ~111k lines) | |
| server=/taobao.com/223.5.5.5 | |
| server=/tmall.com/223.5.5.5 | |
| server=/qq.com/223.5.5.5 | |
| server=/weixin.qq.com/223.5.5.5 | |
| server=/bilibili.com/223.5.5.5 | |
| server=/alipay.com/223.5.5.5 | |
| # ... plus tens of thousands more from dnsmasq-china-list | |
| # Direct-domain allowlist (foreign domains that break through the exit | |
| # node -- Apple/iCloud/game backends). Also resolved via AliDNS so the | |
| # returned IPs match a nearby POP. | |
| server=/apple.com/223.5.5.5 | |
| server=/icloud.com/223.5.5.5 | |
| server=/mzstatic.com/223.5.5.5 | |
| server=/tplinkcloud.com/223.5.5.5 | |
| server=/orerve.net/223.5.5.5 | |
| server=/zaonce.net/223.5.5.5 | |
| # Default resolver for everything else -> stubby DoT -> Cloudflare | |
| # (configured elsewhere in dnsmasq via `server=127.0.0.1#5453`) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/bin/sh | |
| # Toggle the router in and out of "in China" split-routing mode. | |
| ## Leaving China: disable the split | |
| tailscale set --exit-node= --accept-dns=false | |
| uci set pbr.@include[2].enabled='0' # verify index matches pbr.user.china | |
| uci commit pbr | |
| service pbr reload | |
| ## Returning to China: re-enable the split | |
| uci set pbr.@include[2].enabled='1' | |
| uci commit pbr | |
| service pbr reload | |
| tailscale set --exit-node=debian \ | |
| --exit-node-allow-lan-access \ | |
| --accept-dns=false | |
| ## Verify the early rules survived the reload and still land before pref 5270 | |
| ip rule show | grep -E "^(52[6-9][0-9]|5270):" | |
| ## Quick smoke test | |
| ip route get 1.1.1.1 # expect: dev tailscale0 table 52 | |
| ip route get 223.5.5.5 mark 0x30000 # expect: dev sta0 table pbr_wwan |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment