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

Rough configuration overview

Communication Flow

Not quite accurate but design was based on this diagram

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.

If you have domains used only at home, DNS can be directed to specified locations

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:

Where I got stuck the most

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

  1. Access Tailscale Admin Console
  2. Settings → Keys → Generate auth key
  3. 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": ["*:*"] } ] } 

This is how the ACL section looks on screen

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

  1. 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
  2. Check Admin Console settings

    • Is Restrict to domain correctly configured?
    • Is Override local DNS Off?
  3. Check with packet capture

 # Run on Subnet Router side sudo tcpdump -i any -n 'port 53' 

When ping Works But Cannot Access Specific Services

  1. Check DOCKER-USER chain
 sudo iptables -L DOCKER-USER -v -n 
  1. 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

  1. Check Tailscale Release Notes
  2. Verify new version in test environment
  3. Update image tag in docker-compose.yml
  4. 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.