Skip to content

Tailscale VPN

Tailscale ACL policy, peer relay, and connection troubleshooting.

ACL Policy

{
  "tagOwners": {
    "tag:relay": ["autogroup:admin"],
    "tag:clients": ["autogroup:admin"]
  },
  "acls": [
    { "action": "accept", "src": ["*"], "dst": ["*:*"] }
  ],
  "grants": [
    {
      "src": ["tag:clients"],
      "dst": ["tag:relay"],
      "app": { "tailscale.com/cap/relay": [] }
    },
    {
      "src": ["tag:clients"],
      "dst": ["tag:relay"],
      "ip": ["*"]
    }
  ]
}

The grants section must be set via the Tailscale API directly -- the MCP ACL tool (manage_acl) silently drops it:

curl -X POST -H "Authorization: Bearer $TAILSCALE_API_KEY" \
  -H "Content-Type: application/json" \
  "https://api.tailscale.com/api/v2/tailnet/mordasiewicz.com/acl" \
  -d '{ ... full ACL with grants ... }'

Peer Relay

When direct connections fail (school NAT, mobile hotspot symmetric NAT), devices route through tsrelay instead of Tailscale's DERP servers. No bandwidth throttling unlike DERP.

Device tsrelay (Toronto, 100.95.40.19)
Relay port 40000 (tailscale set --relay-server-port=40000)
Port forward Toronto UniFi: UDP 40000 -> tsrelay (192.168.177.228)
Systemd override /etc/systemd/system/tailscaled.service.d/relay.conf
Grant tag:clients -> tag:relay (requires both app and ip grants)

The Toronto Bell PPPoE address is dynamic, and automatic discovery does not reliably advertise the forwarded relay port. The relay therefore advertises the current public IP:40000 explicitly. A systemd timer refreshes it every five minutes when the public address changes:

Updater /usr/local/sbin/tailscale-relay-endpoint-sync
Timer tailscale-relay-endpoint.timer
Check interval 5 minutes

Check the updater and advertised endpoint with:

systemctl status tailscale-relay-endpoint.timer
sudo systemctl start tailscale-relay-endpoint.service
tailscale debug prefs | grep -A3 RelayServer

On 2026-05-24, tsrelay had a stale static endpoint (142.114.208.227:40000). Peer-relay sessions were allocated but stuck at <no handshake>. The timer now prevents this. To clear static endpoint config while troubleshooting:

sudo tailscale set --relay-server-static-endpoints=

Do not leave the endpoint empty afterward. Run the updater service to restore the current public endpoint.

On 2026-07-21, Mac-to-workstation traffic was verified through peer-relay <public-ip>:40000; both clients reported the same VNI and the relay session counters increased in both directions.

Workstation needed Tailscale 1.98.3; with 1.96.4, relay candidates were visible but traffic still preferred DERP in cases where peer relay should have been available.

Connection priority (automatic)

flowchart TD
    A["Client connects"] --> B{"Direct UDP<br/>hole-punch?"}
    B -- "Yes" --> C["Direct connection<br/>~15ms"]
    B -- "No" --> D{"Peer relay<br/>available?"}
    D -- "Yes" --> E["Peer relay<br/>~15-45ms"]
    D -- "No" --> F["DERP relay<br/>~14ms, throttled"]
Priority Type Latency
1 Direct (UDP hole-punch) Fastest
2 Peer relay (tsrelay) ~15ms Toronto-Kingston, no throttling
3 DERP (Tailscale servers) ~14ms, throttled

Grant mechanics

  • src = who can use the relay (tag:clients)
  • dst = the relay device (tag:relay)
  • Only the initiating device needs the tag, not both sides
  • Tags can't be removed without tailscale up --force-reauth
  • To test without relay, remove grants from ACL instead of changing device tags

Connection Results by Location

Mac Location Connection Type Latency Notes
Toronto UniFi Direct ~17ms Requires UPnP enabled on UniFi
Toronto Bell Wi-Fi Direct ~15ms Works naturally (permissive NAT)
School (university) Peer relay ~45ms EDM NAT + no UPnP, direct fails, relay kicks in
Mobile hotspot Peer relay ~50-120ms Carrier CGNAT has symmetric NAT (EDM), no UPnP. Direct to tsrelay may work; workstation goes via peer relay
Anywhere (no relay) DERP Toronto ~14ms Bandwidth throttled

Tailscale Ports

Port Protocol Purpose
41641 UDP Default WireGuard (each device)
40000 UDP Peer relay (tsrelay only)
3478 UDP STUN (NAT discovery)