Accessing Your Home LAN from Outside with Tailscale Subnet Router
I want to access my home NAS or server while away from home. But VPN setup is difficult... I do not have a VPN router either... Tailscale solves these problems.
In this article, I will explain how to build an environment using Docker that allows you to access your entire home LAN from outside using Tailscale Subnet Router functionality.
1. Requirements Overview
What We Want to Achieve
- VPN connection to home LAN (192.168.x.x) from smartphone/PC while away
- Name resolution for internal domains (*.example.local) using home DNS server
- Privacy protection so family members can use it but cannot see each other devices
Users
| User | Usage | Remarks |
|---|---|---|
| Admin | Remote management, all LAN access | Full admin privileges |
| Family | Simple LAN access (NAS, etc.) | Limited privileges, privacy protection between terminals |
Functional Requirements
| Requirement | Description |
|---|---|
| Access to home LAN from outside | Reachable to all devices in 192.168.x.0/24 |
| Inter-user communication blocking | For privacy protection, family devices cannot communicate with each other |
| Split DNS | Only specific domains resolved by home DNS when connected to Tailscale |
| Subnet Router only | Exit Node (all traffic forwarding) not required |
Technical Decisions
| Item | Decision | Reason |
|---|---|---|
| Container runtime | Docker | Easy management, reproducibility |
| Network mode | host | Required for Subnet Router |
| State persistence | Named Volume | Re-authentication unnecessary on restart |
| Health check | Built-in endpoint | Easy monitoring integration |
2. Architecture Overview
Overall Configuration Diagram
Communication Flow
Component Roles
| Component | Role | Remarks |
|---|---|---|
| Tailscale Container | Subnet Router, forwards traffic from Tailnet to LAN | Uses SNAT |
| DNS Server (Unbound) | Internal domain name resolution | Split DNS target |
| UFW/iptables | Firewall, controls forwarding | Important settings required |
Importance of SNAT (Source Network Address Translation)
Tailscale enables SNAT by default (--snat-subnet-routes=true). This is equivalent to MASQUERADE (IP masquerade / NAPT) in Linux iptables.
| Setting | Behavior | Notes |
|---|---|---|
| SNAT enabled (default) | Source IP becomes Subnet Router IP | No additional settings needed on LAN devices |
| SNAT disabled | Tailscale IP (100.x.x.x) is sent as-is | Static routes must be set on LAN routers and devices |
SNAT enabled is recommended. No additional settings needed on LAN devices.
3. Design Considerations
3.1 Understanding Split DNS
Split DNS is a mechanism to resolve only specific domains with specific DNS servers.
Benefits:
- Normal internet access uses fast local DNS
- Only home internal domains resolved by home DNS
- No impact on internet speed even when connected to VPN
3.2 Complex Relationship Between UFW and iptables
This is the biggest pitfall. In environments using Docker, multiple firewall layers exist:
Settings Requiring Attention
| Layer | Setting Location | What to Set |
|---|---|---|
| UFW INPUT | ufw allow | WireGuard port (41641/udp) |
| UFW FORWARD | /etc/default/ufw | DEFAULT_FORWARD_POLICY=ACCEPT |
| DOCKER-USER | /etc/ufw/after.rules | RETURN rules for Tailscale CGNAT |
3.3 DNS Server ACL Settings
If using DNS servers like Unbound, you need to allow queries from Tailscale CGNAT (100.64.0.0/10).
With Split DNS, Tailscale clients send queries directly to the DNS server. At this time, the source IP becomes a CGNAT address (100.x.x.x).
3.4 Auth Key Management
Important: If state is persisted with Docker Named Volume, re-authentication is not required on container restart. Auth Key is only needed in the following cases:
- Initial deployment
- When Named Volume is deleted
- When device is deleted from Tailscale Admin Console
4. Configuration Details
From here, I will explain specific configuration methods.
4.1 Tailscale Admin Console Settings
4.1.1 Issuing Auth Key
- Access Tailscale Admin Console
- Settings → Keys → Generate auth key
- Issue with the following settings:
| Item | Setting |
|---|---|
| Description | home-router |
| Expiration | 90 days (or as needed) |
| Reusable | Off (for security) |
| Ephemeral | Off (persistent device) |
| Pre-approved | On |
| Tags | tag:subnet-router |
Save the issued key securely (format: tskey-auth-xxxxx).
4.1.2 ACL Settings
Set the following in Admin Console → Access Controls:
{
"groups": {
"group:admin": ["your-admin-email@example.com"],
"group:family": ["family-email@example.com"]
},
"tagOwners": {
"tag:subnet-router": ["group:admin"]
},
"hosts": {
"home_network": "192.168.x.0/24"
},
"acls": [
{
"action": "accept",
"src": ["group:admin", "group:family"],
"dst": ["home_network:*"]
},
{
"action": "accept",
"src": ["tag:subnet-router"],
"dst": ["*:*"]
}
]
}
Points:
- group:admin and group:family can access home LAN
- By not defining inter-terminal communication rules, default deny is applied and mutual communication is blocked
4.1.3 DNS Settings (After Deployment)
Set the following in Admin Console → DNS:
| Item | Setting |
|---|---|
| Add Nameserver | Custom: 192.168.x.53 (home DNS server IP) |
| Restrict to domain | example.local |
| Override local DNS | Off |
4.2 Server-Side Preparation
4.2.1 Enabling IP Forwarding
# Create configuration file
sudo tee /etc/sysctl.d/99-tailscale.conf << 'EOF'
net.ipv4.ip_forward = 1
net.ipv6.conf.all.forwarding = 1
EOF
# Apply immediately
sudo sysctl -p /etc/sysctl.d/99-tailscale.conf
# Verify
sysctl net.ipv4.ip_forward
# Output: net.ipv4.ip_forward = 1
4.2.2 TUN Device Verification
# Verify TUN device exists
ls -la /dev/net/tun
# Create if it does not exist
sudo mkdir -p /dev/net
sudo mknod /dev/net/tun c 10 200
sudo chmod 666 /dev/net/tun
4.3 Docker Configuration
4.3.1 Directory Creation
sudo mkdir -p /opt/tailscale
cd /opt/tailscale
4.3.2 Creating docker-compose.yml
sudo tee /opt/tailscale/docker-compose.yml << 'EOF'
services:
tailscale:
image: tailscale/tailscale:v1.94.1
container_name: tailscale
hostname: home-router
restart: unless-stopped
cap_add:
- NET_ADMIN
- NET_RAW
network_mode: host
volumes:
- tailscale_data:/var/lib/tailscale
- /dev/net/tun:/dev/net/tun
environment:
- TS_AUTHKEY=tskey-auth-xxxxxxx # Set Auth Key here
- TS_STATE_DIR=/var/lib/tailscale
- TS_ROUTES=192.168.x.0/24
- TS_USERSPACE=false
- TS_ACCEPT_DNS=false
- TS_EXTRA_ARGS=--port=41641
- TS_ENABLE_HEALTH_CHECK=true
- TS_ENABLE_METRICS=true
- TS_LOCAL_ADDR_PORT=[::]:9002
volumes:
tailscale_data:
EOF
Configuration Item Descriptions:
| Item | Value | Description |
|---|---|---|
| image | tailscale/tailscale:v1.94.1 | Use specific version |
| network_mode | host | Required for Subnet Router |
| cap_add | NET_ADMIN, NET_RAW | Network operation privileges |
| TS_ROUTES | 192.168.x.0/24 | Subnet to advertise |
| TS_USERSPACE | false | Use kernel mode (recommended) |
| TS_ACCEPT_DNS | false | Do not use Tailscale DNS on server side |
Important Settings:
| Environment Variable | Description | Notes |
|---|---|---|
| TS_AUTHKEY | Authentication key | Needed only for initial deployment |
| TS_STATE_DIR | State storage location | Authentication state is persisted |
| TS_ROUTES | Advertised routes | LAN subnet to be accessible |
4.3.3 Starting the Container
cd /opt/tailscale
sudo docker compose up -d
# Check logs
sudo docker compose logs -f
4.3.4 Operation Verification
# Check container status
sudo docker ps | grep tailscale
# Check Tailscale status
sudo docker exec tailscale tailscale status
# Expected output example:
# 100.x.x.x home-router your-tailnet.ts.net linux -
# 100.x.x.x iphone your-tailnet.ts.net iOS idle
4.3.5 Route Approval
After starting the container, enable 192.168.x.0/24 in Tailscale Admin Console → Machines → home-router → Edit route settings.
4.4 iptables/UFW Configuration
4.4.1 Changing UFW Routed Policy
Since Subnet Router forwards packets, UFW routed policy needs to be changed to allow.
# Check current settings
sudo ufw status verbose
# Default: deny (incoming), allow (outgoing), deny (routed) ← routed is deny
# Edit /etc/default/ufw
sudo sed -i 's/DEFAULT_FORWARD_POLICY="DROP"/DEFAULT_FORWARD_POLICY="ACCEPT"/' /etc/default/ufw
# Reload UFW
sudo ufw reload
# Verify
sudo ufw status verbose
# Default: deny (incoming), allow (outgoing), allow (routed) ← OK if allow
4.4.2 Allowing WireGuard Port
sudo ufw allow 41641/udp comment "Tailscale WireGuard"
4.4.3 DOCKER-USER Chain Configuration (Important)
When using Docker, the DOCKER-USER chain filters traffic from outside. Traffic from Tailscale CGNAT (100.64.0.0/10) needs to be allowed.
# Edit /etc/ufw/after.rules
sudo tee -a /etc/ufw/after.rules << 'EOF'
# BEGIN TAILSCALE DOCKER INTEGRATION
*filter
:DOCKER-USER - [0:0]
-A DOCKER-USER -j RETURN -s 10.0.0.0/8
-A DOCKER-USER -j RETURN -s 172.16.0.0/12
-A DOCKER-USER -j RETURN -s 192.168.0.0/16
-A DOCKER-USER -j RETURN -s 100.64.0.0/10
-A DOCKER-USER -j DROP -p tcp -m tcp --tcp-flags FIN,SYN,RST,ACK SYN -d 192.168.0.0/16
-A DOCKER-USER -j DROP -p tcp -m tcp --tcp-flags FIN,SYN,RST,ACK SYN -d 10.0.0.0/8
-A DOCKER-USER -j DROP -p tcp -m tcp --tcp-flags FIN,SYN,RST,ACK SYN -d 172.16.0.0/12
-A DOCKER-USER -j DROP -p udp -m udp --dport 0:32767 -d 192.168.0.0/16
-A DOCKER-USER -j DROP -p udp -m udp --dport 0:32767 -d 10.0.0.0/8
-A DOCKER-USER -j DROP -p udp -m udp --dport 0:32767 -d 172.16.0.0/12
-A DOCKER-USER -j RETURN
COMMIT
# END TAILSCALE DOCKER INTEGRATION
EOF
# Reload UFW
sudo ufw reload
# Verify
sudo iptables -L DOCKER-USER -v -n
Important: Without -A DOCKER-USER -j RETURN -s 100.64.0.0/10, UDP packets (including DNS) from Tailscale clients to LAN will be dropped.
4.5 Unbound (DNS Server) Configuration
When using Split DNS, the DNS server needs to allow queries from Tailscale CGNAT (100.64.0.0/10).
4.5.1 Unbound ACL Configuration
# Edit /etc/unbound/unbound.conf.d/access-control.conf
sudo tee /etc/unbound/unbound.conf.d/access-control.conf << 'EOF'
server:
# Localhost
access-control: 127.0.0.0/8 allow
# LAN
access-control: 192.168.x.0/24 allow
# Docker networks
access-control: 172.16.0.0/12 allow
# Tailscale CGNAT (for Split DNS)
access-control: 100.64.0.0/10 allow
# Deny all others
access-control: 0.0.0.0/0 refuse
EOF
# Restart Unbound
sudo systemctl restart unbound
# Verify
sudo systemctl status unbound
4.5.2 DNS Server UFW Configuration
If the DNS server is on a separate machine, UFW on that machine also needs settings to allow DNS queries from Tailscale CGNAT.
# Run on DNS server side
sudo ufw allow from 100.64.0.0/10 to any port 53 proto tcp comment "DNS TCP from Tailscale"
sudo ufw allow from 100.64.0.0/10 to any port 53 proto udp comment "DNS UDP from Tailscale"
5. Verification Procedures
5.1 Server-Side Verification
# 1. Container startup verification
sudo docker ps | grep tailscale
# Expected: tailscale container in Up state
# 2. Tailscale authentication verification
sudo docker exec tailscale tailscale status
# Expected: home-router displayed with 192.168.x.0/24 in Routes
# 3. IP forwarding verification
sysctl net.ipv4.ip_forward
# Expected: net.ipv4.ip_forward = 1
# 4. Health check
curl http://localhost:9002/healthz
# Expected: 200 OK
5.2 Client-Side Verification
# 1. Tailscale connection verification
tailscale status
# Expected: Connection successful, 100.x.x.x IP obtained
# 2. LAN connectivity verification
ping 192.168.x.10
# Expected: Response received
# 3. DNS verification (Split DNS)
dig grafana.example.local
# Expected: Returns internal IP like 192.168.x.10
# 4. Service access
curl https://grafana.example.local/
# Expected: Normal response
5.3 Security Verification
# Inter-terminal communication blocking verification (from Admin device to Family device)
ping 100.x.x.x # Tailscale IP of Family device
# Expected: Timeout (communication not possible)
6. Additional Notes
6.1 Important Settings Not Documented in Design Documents
Important configuration items discovered during actual construction that were missing from documentation:
6.1.1 DNS Server UFW Rules
- Problem: DNS queries timeout even with Split DNS configured
- Cause: DNS server UFW was blocking queries from Tailscale CGNAT (100.64.0.0/10)
- Solution: Add the following to DNS server UFW
sudo ufw allow from 100.64.0.0/10 to any port 53 proto tcp
sudo ufw allow from 100.64.0.0/10 to any port 53 proto udp
6.1.2 DOCKER-USER Chain Configuration
- Problem: ping works but DNS (UDP port 53) times out
- Cause: DROP rules in DOCKER-USER chain were dropping UDP packets from Tailscale
-A DOCKER-USER -j DROP -p udp --dport 0:32767 -d 192.168.0.0/16
- Solution: Add the following to /etc/ufw/after.rules
-A DOCKER-USER -j RETURN -s 100.64.0.0/10
6.1.3 UFW Routed Policy
- Problem: Tailscale connects but all communication to LAN times out
- Cause: UFW routed policy was deny by default, blocking packet forwarding
-
Solution: Change to
DEFAULT_FORWARD_POLICY="ACCEPT"in /etc/default/ufw
6.2 Troubleshooting
When Split DNS Does Not Work
-
Check client settings
- iOS/Android: Check if Use Tailscale DNS is enabled in Tailscale app settings
- macOS/Windows: Check for conflicts between system DNS settings and Tailscale settings
-
Check Admin Console settings
- Is Restrict to domain correctly configured?
- Is Override local DNS Off?
Check with packet capture
# Run on Subnet Router side
sudo tcpdump -i any -n 'port 53'
When ping Works But Cannot Access Specific Services
- Check DOCKER-USER chain
sudo iptables -L DOCKER-USER -v -n
- Check if RETURN rule for 100.64.0.0/10 exists
Auth Key Expiration
Since state is maintained with Named Volume, this is usually not a problem. However, a new Auth Key is required in the following cases:
- When Named Volume is deleted
- When device is deleted from Tailscale Admin Console
6.3 Operational Notes
Auth Key Rotation
| Scenario | Required Action |
|---|---|
| Container restart | None (state is persisted) |
| Named Volume deletion | Issue new Auth Key, restart container |
| Device deletion from Admin Console | Issue new Auth Key, restart container |
Version Update Procedure
- Check Tailscale Release Notes
- Verify new version in test environment
- Update image tag in docker-compose.yml
docker compose pull && docker compose up -d
6.4 Reference Links
This completes the Tailscale Subnet Router construction. You can now securely access your home LAN from outside.





Top comments (3)
Could sections 3.3/4.5 distinguish the DNS server's location and SNAT mode? With the shown default SNAT, a separate LAN DNS server reached through the subnet route normally sees the subnet router's source address, not the original 100.x client. Tailscale documents that distinction here: tailscale.com/docs/features/subnet...
A capture on the DNS server would establish the address its ACL actually needs; traffic terminated on the router itself is a different path. That would keep readers from widening the DNS ACL based only on the client's Tailscale address. I haven't tested this topology.
AI-assisted technical feedback from Lisar, a VPN service.
Thanks for the detailed feedback! You're right — the article correctly describes SNAT in Section 2, but Sections 3.3 and 4.5 don't consistently account for it.
With default SNAT enabled, a separate DNS server on the LAN reached through the advertised subnet normally sees the subnet router's LAN IP, not the original Tailscale client's 100.x address.
I'll clarify the distinction between a separate LAN DNS server and DNS running on the subnet router itself, update the Unbound/UFW examples, and add a packet-capture step to verify the actual source IP before configuring DNS ACLs.
Thanks for pointing this out and linking the official documentation!
Dùng subnet router cách đây vài tháng cho homelab (Proxmox + LXC), thấy một vài điểm đáng note thêm:
MTU — mặc định 1280 an toàn, nhưng nếu nhà bạn chạy WireGuard native trên router (OpenWrt/OPNsense) thì bump lên 1420 cho throughput tốt hơn. Test
ping -s 1400 -M do <tailscale-ip>trước khi commit.Route advertisement — bật
accept-routestrên client vàadvertise-routestrên node subnet. Quên cái nào mất một bên là traffic không đi được, log chỉ hiện "no route to host" rất dễ nhầm lẫn với firewall.Exit node vs subnet router — đừng bật cả hai trên cùng một node trừ khi cần forced egress. Subnet router chỉ announce LAN CIDR, exit node thì capture all traffic. Nhầm lẫn dẫn đến leak DNS hoặc break local service discovery.
Key expiry — ephemeral key cho CI/CD deploy subnet router (GitHub Actions → Tailscale auth key reusable=false, ephemeral=true, tags=tag:subnet-router). Rotate tự động, không lo secret dính repo.
Health check — thêm systemd timer ping subnet gateway mỗi 5p, fail thì `tailscale — found it via LabAgent, site: labagent .tech