Skip to content

Tailscale - Street-Level Ops

Quick Diagnosis Commands

# Check node status and connected peers
tailscale status

# Test connectivity to a specific tailscale node
tailscale ping <hostname-or-ip>

# Show current IP and hostname
tailscale ip
tailscale ip -4    # IPv4 only
tailscale ip -6    # IPv6 only

# Full status with peer details
tailscale status --json | jq .

# Check if tailscale is up
tailscale status --json | jq '.BackendState'

# List all peers and their online status
tailscale status --json | jq -r '.Peer[] | "\(.HostName)\t\(.TailscaleIPs[0])\t\(.Online)"'

# Show currently active exit node
tailscale status --json | jq '.ExitNodeStatus'

# Debug network connectivity issues
tailscale netcheck

> **One-liner:** `tailscale netcheck` is your first diagnostic. It tests DERP relay connectivity, NAT type, and UDP availability in one command. If `MappingVariesByDestIP: true`, you have symmetric NAT and direct connections won't work — traffic will always relay through DERP.

# Check key expiry
tailscale status --json | jq -r '.Peer[] | select(.KeyExpiry != null) | "\(.HostName): expires \(.KeyExpiry)"'

# Show Tailscale routes (subnet routes)
tailscale status --json | jq -r '.Peer[] | select(.PrimaryRoutes != null) | "\(.HostName): \(.PrimaryRoutes[])"'
# Bring tailscale up with specific options
tailscale up --authkey=<key>

# Bring up with subnet routing
tailscale up --advertise-routes=10.0.0.0/8,192.168.1.0/24

# Bring up as exit node
tailscale up --advertise-exit-node

# Bring up with hostname override
tailscale up --hostname=my-server-prod

# Bring up with tags (for ACL matching)
tailscale up --advertise-tags=tag:server,tag:prod

# Bring up with custom DNS
tailscale up --accept-dns=true

# Disconnect temporarily (keeps config)
tailscale down

# Logout and remove node from network
tailscale logout
# Send a file to a tailscale peer
tailscale file cp ./report.pdf my-laptop:

# Receive files
tailscale file get ~/Downloads/

# Check file transfer status
tailscale file status

# Connect to a node via Tailscale SSH
ssh user@hostname.tail-domain.ts.net
# or by tailscale IP
ssh user@100.x.x.x

# Use tailscale funnel to expose local port (HTTPS, publicly accessible)
tailscale funnel 8080

# Check funnel status
tailscale funnel status

Common Scenarios

Scenario 1: Node shows offline but machine is running

# On the unreachable node, check daemon status
systemctl status tailscaled
journalctl -u tailscaled -n 50

# Check if daemon is listening
tailscale status

# If daemon died, restart it
systemctl restart tailscaled

# Check for key expiry
tailscale status --json | jq '.Self.KeyExpiry'

# If key is expired, reauthenticate
tailscale up    # prompts for new auth

# Run netcheck to verify relay connectivity
tailscale netcheck

# Check if the node can reach DERP relays
tailscale debug derp-map | jq '.Regions[].Nodes[] | {hostname, ipv4}'

Scenario 2: Subnet router not working — devices on the subnet unreachable

# Verify the router node is advertising routes
tailscale status --json | jq '.Self.AdvertisedRoutes'

# Routes must also be APPROVED in the admin console
# Check if routes are approved (enabled):
tailscale status --json | jq '.Self.PrimaryRoutes'
# PrimaryRoutes shows what's actually active; AdvertisedRoutes shows what's offered

> **Default trap:** Advertising a subnet route is not enough  routes must be explicitly approved in the Tailscale admin console. And on the client side, `--accept-routes` must be passed to `tailscale up`. Three separate steps that all must be correct: advertise, approve, accept.

# On the client side, check accepted routes
tailscale status --json | jq '.Peer[] | select(.HostName=="my-router") | .PrimaryRoutes'

# Enable route acceptance on client
tailscale up --accept-routes

# Verify IP forwarding is on the subnet router
cat /proc/sys/net/ipv4/ip_forward   # must be 1
sysctl -w net.ipv4.ip_forward=1
# Make permanent:
echo "net.ipv4.ip_forward = 1" >> /etc/sysctl.d/99-tailscale.conf

# Test from client — can you ping a device behind the subnet router?
tailscale ping 192.168.1.50   # direct test through tailscale

Scenario 3: ACL policy blocking connections

# Check if traffic is being blocked (ACL issue shows as timeout, not refused)
tailscale ping <peer>   # if this works, layer 3 is fine
# Try the actual port:
curl -v http://<tailscale-ip>:8080/  # if this hangs, ACL is blocking

# Debug ACL enforcement
tailscale debug access <dst-ip> <port> <proto>
# Example:
tailscale debug access 100.64.1.5 80 tcp

# MagicDNS not resolving? Check
tailscale status --json | jq '.MagicDNSSuffix'
# Test resolution:
dig @100.100.100.100 my-server.tail-xyz.ts.net

# Split DNS: check which DNS servers are assigned for which domains
tailscale status --json | jq '.DNS'

Scenario 4: Slow performance — forcing direct connection instead of relay

# Check if connection is direct or relayed (DERP)
tailscale status --json | jq -r '.Peer[] | "\(.HostName): \(.Relay) \(if .CurAddr != "" then "DIRECT:" + .CurAddr else "RELAYED" end)"'

# Force a re-probe for direct path
tailscale ping --until-direct <peer>

# Check what's blocking direct: firewall, NAT type
tailscale netcheck
# Look for: "MappingVariesByDestIP: true" (symmetric NAT, will use relay)
# Look for: "HairPinning: false" (devices on same network can't talk directly)

# Check latency via relay vs expected direct
tailscale ping --count 10 <peer>

# If stuck on relay, check UDP 41641 is open outbound on both sides
# Tailscale uses UDP 41641 for direct connections by default

> **Debug clue:** If `tailscale ping --until-direct <peer>` never transitions from "via DERP" to a direct address, both sides likely have restrictive NAT or a firewall blocking UDP 41641 outbound. Corporate firewalls are the most common cause  Tailscale works over DERP relay in this case, but latency will be higher.

Key Patterns

Deploying a subnet router

1. On the router node:
   tailscale up --advertise-routes=10.0.0.0/24 --accept-routes

2. Enable IP forwarding:
   sysctl -w net.ipv4.ip_forward=1
   echo "net.ipv4.ip_forward=1" > /etc/sysctl.d/99-tailscale.conf

3. In Tailscale admin console (admin.tailscale.com):
   Machines -> select node -> Edit route settings -> approve the advertised routes

4. Verify from another tailnet node:
   tailscale status --json | jq '.Peer[] | select(.HostName=="router") | .PrimaryRoutes'
   ping 10.0.0.1    # device behind the subnet router

Setting up exit node

1. On the exit node:
   tailscale up --advertise-exit-node

2. Approve in admin console:
   Machines -> select node -> Edit route settings -> enable "Use as exit node"

3. On client to use the exit node:
   tailscale up --exit-node=<hostname-or-ip>
   # or via GUI

4. Verify:
   curl https://ifconfig.me   # should show exit node's public IP

5. To stop using exit node:
   tailscale up --exit-node=

Managing key expiry

1. Check which nodes have expiring keys:
   tailscale status --json | jq -r '.Peer[] | select(.KeyExpiry != null) | "\(.HostName): \(.KeyExpiry)"'

2. For servers that must stay connected, disable key expiry in admin console:
   Machines -> select node -> Disable key expiry

3. For service accounts, use auth keys with long TTL or disable expiry:
   # In admin console: Settings -> Keys -> Generate auth key
   # Check "Reusable" and "No expiry" for long-lived service nodes

4. To reauthenticate an expired node:
   tailscale up    # prompts for login if key expired
   # or with auth key:
   tailscale up --authkey=<new-key>

ACL policy workflow

1. Always test ACL changes in a staging tailnet first.

2. Format: HuJSON (JSON with comments)
   Key blocks: hosts, groups, tagOwners, acls, ssh, tests

3. Validate before saving:
   - Use the "Test" button in admin console
   - Or use tailscale debug access to test from CLI

4. Common ACL patterns:
   # Allow all access within a group:
   {"action": "accept", "src": ["group:devs"], "dst": ["*:*"]}

   # Allow only specific ports to tagged servers:
   {"action": "accept", "src": ["group:devs"], "dst": ["tag:prod:22,80,443"]}

5. Deny is implicit  everything not allowed is denied.
   If you cannot connect, check ACLs before anything else.

> **Gotcha:** ACL changes take effect immediately across the entire tailnet  there is no staging or gradual rollout. A syntax error in your ACL file can instantly lock every node out of every other node. Always use the "Test" button in the admin console before saving, and keep a browser tab open to the admin console so you can revert if needed.