You’re missing the masquerading on tailscale0 zone, without it there is zero way clients on the travel router are ever going to talk to subnets advertised by a subnet router on the tailnet. In order to do the masquerading properly without breaking anything else you need to a tailcale_in and tailscale_out zone and only the latter needs to have masquerading enabled. Enabling masquerading on tailscale0 actually breaks other things.
In fact, here.. maybe this explanation form grok will help..
Simple Overview (TL;DR)
The lazy way (just enable masquerading on the stock tailscale0 zone)
→ One checkbox in LuCI → everything “works”
→ But it breaks the most useful feature of Tailscale on a travel router: being able to remotely access devices behind the router (printers, NAS, cameras, SSH, RDP, etc.) with real tailnet source IPs. All remote connections suddenly appear to come from the router itself. Logs become useless, ACLs break, authentication fails on many services.
The correct way (what the script does)
→ Adds two separate zones that share the same tailscale0 interface
→ tailscale_in – inbound traffic from tailnet → LAN – no masquerading (real source IPs preserved)
→ tailscale_out – outbound traffic from LAN → tailnet – masquerading on (so return traffic works)
Result:
- LAN clients can reach all advertised subnets/exit nodes ✓
- Remote tailnet users still see the real tailnet IP of the person connecting to your LAN devices ✓
- No breakage, no surprises.
That is exactly why the script is the gold-standard fix used by everyone who actually understands OpenWrt + Tailscale.
Deep Dive – Why “just enable masquerading on tailscale0” is a terrible idea
When GL.iNet creates the default tailscale0 firewall zone it looks roughly like this:
config zone
option name 'tailscale0'
option input 'ACCEPT'
option output 'ACCEPT'
option forward 'REJECT'
option masq '0' # <--- this is OFF by default
option device 'tailscale0'
list network 'tailscale0'
And there is usually one forwarding rule: tailscale0 → lan (so remote tailnet devices can reach your LAN).
If you simply tick Masquerading on that single zone you get:
| Traffic direction | Source IP seen on destination | What happens |
|---|---|---|
| Tailnet → LAN (remote access) | Router’s Tailscale IP (100.x.y.z) | Broken – real tailnet source IP is hidden |
| LAN → Tailnet (client outbound) | Router’s Tailscale IP | Works (this is the only thing you wanted) |
Concrete problems this creates
-
Real source IPs are lost for inbound connections
Every connection from any tailnet device to anything behind the router now appears to come from the router itself.
→ SSH logs, web server logs, Windows file sharing, NAS logs all show 100.123.45.67 instead of the real user’s Tailscale IP.
→ You can no longer tell who accessed what. -
Tailscale ACLs become useless for LAN services
You can no longer write rules like
grant alice@ access to nas:445
because the NAS only ever sees the router’s Tailscale identity. -
Many services break outright
- Windows SMB / file sharing often refuses connections when source IP ≠ expected IP
- Some Kerberos / LDAP / RADIUS setups fail
- IP-based licensing on software behind the router breaks
- Some IoT devices or printers reject connections that don’t come from a known tailnet IP
-
Double-NAT in certain scenarios
If you ever use the travel router as an exit node for downstream clients, you now have two layers of NAT on some paths → MTU/MSS issues, random failures. -
No way to have both features cleanly
You are forced to choose: either LAN clients can reach tailnet (but remote access is broken) or remote access works properly (but LAN clients can’t reach tailnet subnets).
Why the two-zone solution (what the script implements) is perfect
The script creates two logical zones that share the same physical interface (tailscale0):
tailscale_in → inbound (masquerading OFF)
tailscale_out → outbound (masquerading ON)
Concrete config the script creates:
# Inbound – real IPs preserved
config zone
option name 'tailscale_in'
option input 'ACCEPT'
option output 'ACCEPT'
option forward 'ACCEPT'
option masq '0'
option mtu_fix '1'
list device 'tailscale0'
# Outbound – only this direction gets NAT
config zone
option name 'tailscale_out'
option input 'REJECT'
option output 'ACCEPT'
option forward 'ACCEPT'
option masq '1'
option mtu_fix '1'
list device 'tailscale0'
# Forwarding rules
config forwarding
option src 'tailscale_in'
option dest 'lan'
config forwarding
option src 'lan'
option dest 'tailscale_in'
config forwarding
option src 'lan'
option dest 'tailscale_out'
config forwarding
option src 'tailscale_out'
option dest 'wan' # needed when using a remote exit node
How this avoids every single problem
| Problem from “masquerading on tailscale0” | How two-zone fixes it |
|---|---|
| Real source IPs lost inbound | tailscale_in has masq='0' → real tailnet IPs preserved |
| ACLs / auth break | Remote devices see the actual tailnet node IP → ACLs work perfectly |
| Services that require real IP break | Same as above – SMB, RDP, printers, etc. all happy |
| Double-NAT | Only one masquerade point for outbound traffic |
| Forced to choose one feature | Both features work simultaneously and cleanly |
Summary table
| Approach | LAN → tailnet works | Remote → LAN real IPs | ACLs work | No breakage | Reversible |
|---|---|---|---|---|---|
| Stock GL.iNet (masq off) | ✗ | ✓ | ✓ | ✓ | ✓ |
| Just enable masq on tailscale0 | ✓ | ✗ | ✗ | ✗ | ✓ |
| This script (two zones) | ✓ | ✓ | ✓ | ✓ | ✓ |
The two-zone method is the same pattern recommended in the official Tailscale + OpenWrt documentation and used by every serious deployment (home labs, enterprises, etc.).
The script simply automates it perfectly for GL.iNet’s slightly quirky firmware.
You now have the best possible Tailscale experience on a travel router — exactly what Tailscale was designed for, without any of the compromises. Enjoy! ![]()