← blog

Tailscale Explained: A Mesh VPN With No Open Ports

24 min homelab & networking tailscale, vpn, networking, homelab, wireguard

The first time you SSH into your home server from a coffee shop without opening a port on your router, it’s hard to see how it worked. There’s no forwarded port, no static IP, and nothing listening on the public internet. This post is about what actually happened.

Tailnet mesh diagram A mesh diagram showing devices connected through a tailnet with encrypted peer-to-peer links. Tailnet mesh (simplified) Every device connects peer-to-peer through the encrypted WireGuard mesh Tailscale coordination Laptop 100.1.2.3 Home server 100.3.4.5 Phone 100.5.6.7 Work PC 100.7.8.9 Tablet 100.10.11.12 Encrypted peer-to-peer WireGuard tunnels

What Tailscale is (and is not)

Tailscale is a mesh VPN built on WireGuard. Instead of routing all your traffic through a single VPN server, it creates direct, encrypted connections between your devices whenever possible. Think of it as a private mesh network with strong identity controls.

If you’re new to VPNs entirely, start here: What Is a VPN?

The short version of how it works

  1. You install the app on devices you own.
  2. You log in with an identity provider (Google, GitHub, etc.).
  3. Tailscale brokers the connection so your devices can find each other.
  4. Devices connect directly (peer-to-peer) whenever possible.

Under the hood, it uses a mix of STUN and NAT traversal to discover how each device can be reached. If the network won’t allow it, Tailscale falls back to relays (DERP).

WireGuard under the hood

Tailscale uses WireGuard, which is fast, minimal, and audited. WireGuard gives you:

Here’s WireGuard’s official paper for the curious: WireGuard: Next Generation Kernel Network Tunnel

How WireGuard’s cryptography works The Noise protocol handshake, step by step

How WireGuard key exchange actually works

WireGuard uses the Noise Protocol Framework, specifically the Noise_IKpsk2 handshake pattern. Here’s what that means in practice.

When two devices want to talk:

  1. Each device has a static key pair — a long-term Curve25519 public/private key. This is your device’s cryptographic identity.

  2. The initiator generates an ephemeral key pair — a temporary keypair used only for this handshake. This provides forward secrecy: even if your long-term keys get compromised later, past sessions can’t be decrypted.

  3. Diffie-Hellman happens (multiple times):

    • Initiator’s ephemeral × Responder’s static
    • Initiator’s static × Responder’s static
    • Initiator’s ephemeral × Responder’s ephemeral
  4. All those DH outputs get mixed together using HKDF to derive the actual symmetric encryption keys.

  5. Optional pre-shared key gets mixed in too (the psk2 part), adding quantum resistance if you’re paranoid about future quantum computers.

The whole handshake is 1-RTT (one round trip) and fits in a single UDP packet each way. Compare that to TLS, which needs multiple round trips and way more complexity.

WireGuard Noise_IKpsk2 Handshake (1-RTT) Initiator (Device A) Responder (Device B) generates ephemeral keypair e_i 1. Initiation msg: e_i(pub), enc{ s_i(pub) }, enc{ timestamp } DH1 = e_i × s_r DH2 = s_i × s_r generates ephemeral keypair e_r 2. Response msg: e_r(pub), enc{ empty } DH3 = e_i × e_r → mix all DH via HKDF → mix pre-shared key (psk2) → symmetric session keys (ChaCha20-Poly1305) 3. Encrypted transport data (forward-secret, E2E)

The Noise_IKpsk2 handshake: three Diffie-Hellman exchanges plus an optional PSK, all mixed via HKDF into forward-secret session keys — in a single round trip. Open the editable Excalidraw source.

For the full cryptographic details, see the WireGuard protocol documentation.

Control plane vs data plane (why it’s still fast)

Tailscale has a control plane that helps devices discover each other and exchange keys, but the data plane (the actual traffic) goes directly between your devices when possible. That’s why it feels fast — you’re not hair-pinning through a central server unless you have to.

Because only metadata flows through the control plane, the coordination service never sees your actual encrypted traffic. If you self-host with Headscale, you take over that coordination role.

Control Plane vs Data Plane Coordination server (CONTROL PLANE) keys · ACLs · peer discovery metadata only, never your traffic Device A Device B Device C DATA PLANE: direct encrypted P2P (WireGuard, end-to-end) control plane (metadata) data plane (your traffic)

The coordination server only brokers metadata (keys, ACLs, peer discovery). Your actual traffic flows directly device-to-device over the data plane. Open the editable Excalidraw source.

Tailscale vs Headscale

If you already run a homelab and want to minimize third-party trust, Headscale is worth a look.

How Tailscale compares to traditional VPN architectures

Traditional VPNs and Tailscale solve different problems, and comparing them head-to-head only makes sense once that’s clear.

Traditional hub-and-spoke VPN (OpenVPN, IPsec, etc.)

          ┌─────────────┐
          │  VPN Server │
          │  (gateway)  │
          └──────┬──────┘
       ┌─────────┼─────────┐
       │         │         │
       ▼         ▼         ▼
    Device A  Device B  Device C

Tailscale’s mesh architecture

    Device A ◄────────► Device B
        ▲                   ▲
        │                   │
        └───────┬───────────┘
                │
                ▼
            Device C
    
    (Control plane handles discovery,
     but data flows directly)

The “site-to-site” problem

Traditional VPNs excel at connecting networks (your office LAN to the datacenter). Tailscale excels at connecting devices regardless of what network they’re on. Different tools, different jobs.

If you need a contractor to access one internal service without touching the rest of your network, Tailscale wins. If you need to bridge two entire subnets with legacy equipment that can’t run clients, traditional VPN (or Tailscale’s subnet routers) might be the answer.

Identity + keys (the real security story)

Every device gets a unique key pair, and access is tied to your identity provider. That’s the zero-trust feel: instead of trusting a network, you trust who (and which device) is connecting. Under the hood it’s standard public-key cryptography, but with sane defaults.

Tailscale also rotates keys over time, which limits the impact of a long-lost laptop or a compromised device.

A note on identity provider trust

Here’s the uncomfortable truth: your tailnet is only as secure as your identity provider. If someone compromises your Google or GitHub account, they can add their own device to your tailnet and access everything your ACLs allow. This is the trade-off for the convenience of SSO.

Mitigations worth considering:

NAT traversal + DERP relays (the “how did it work?” part)

Most home networks sit behind NAT, which makes inbound connections hard. Tailscale uses NAT traversal techniques to punch through when it can. If that fails, it falls back to DERP relays (basically a secure relay network) so your devices still connect.

Relays are a fallback — not the default. You can check in the Tailscale admin panel whether a connection is direct or relayed.

How NAT hole-punching actually works (step by step)

NAT hole-punching sounds like hacker movie nonsense, but it’s actually elegant. Here’s what happens when Device A (behind NAT) wants to connect to Device B (also behind NAT):

Step 1: STUN discovery

Both devices send packets to Tailscale’s STUN servers. The STUN server sees the external IP and port that the NAT assigned, and reports it back. Now each device knows its own public-facing address.

Device A ──► NAT A ──► STUN Server
                           │
            "Your public address is 1.2.3.4:54321"

Step 2: Coordination server exchange

Tailscale’s control plane tells each device the other’s public address. Device A learns that Device B is at 5.6.7.8:12345, and vice versa.

Step 3: Simultaneous connection attempts

Here’s the magic: both devices send packets to each other at roughly the same time.

Device A (1.2.3.4:54321) ──────► Device B (5.6.7.8:12345)
Device B (5.6.7.8:12345) ──────► Device A (1.2.3.4:54321)

When Device A sends a packet out to Device B, NAT A creates a mapping: “packets from Device A going to 5.6.7.8:12345 should use external port 54321.”

When Device B’s packet arrives at NAT A, it looks like a reply to Device A’s outbound packet — so NAT A lets it through. The same happens in reverse at NAT B.

Step 4: Connection established

Both NATs have now “punched holes” for this specific peer. Traffic flows directly.

For more details, see Tailscale’s excellent How NAT traversal works blog post.

NAT Traversal: STUN Hole-Punching with DERP Fallback NAT A NAT B Device A Device B STUN server Coordination server (control) DERP relay 1. STUN: learn my public ip:port 1. STUN: learn my public ip:port 2. register + exchange peer public addresses (metadata) 3. simultaneous send = hole punch → direct P2P 4. fallback when hole-punch fails (symmetric NAT / UDP blocked): encrypted WireGuard blobs relayed — DERP can’t read them

STUN discovery and simultaneous sends punch a direct path through both NATs. When symmetric NAT or a UDP-blocking firewall gets in the way, encrypted traffic falls back to a DERP relay — which still can’t read the WireGuard payload. Open the editable Excalidraw source.

DERP relay infrastructure: what’s actually happening

DERP stands for Designated Encrypted Relay for Packets (yes, really). When direct connections fail, your traffic routes through DERP servers — but here’s the key thing: DERP can’t read your traffic. It’s still WireGuard-encrypted end-to-end; DERP just forwards the opaque blobs.

Tailscale operates DERP servers worldwide:

When you run tailscale netcheck, you’ll see latency to each DERP region. The client automatically picks the fastest one.

shell
$ tailscale netcheck

Report:
    * UDP: true
    * IPv4: yes, 1.2.3.4:54321
    * IPv6: yes, [2001:db8::1]:54321
    * MappingVariesByDestAddr: false
    * HairPinning: false
    * PortMapping: UPnP
    * Nearest DERP: San Francisco
    * DERP latency:
        - sfo: 12ms
        - lax: 18ms
        - sea: 25ms
        - nyc: 65ms
        - fra: 142ms

Tailscale Funnel: public access without the port forwarding nightmare

Here’s a feature that made me delete my Cloudflare Tunnel setup: Tailscale Funnel. It lets you expose a service on your tailnet to the public internet — no port forwarding, no dynamic DNS, no reverse proxy configuration.

How it works

Public Internet ──► Tailscale's edge ──► Your device (via DERP/direct)
    https://mybox.tail1234.ts.net
  1. Tailscale provisions a public hostname for your device (like mybox.tail1234.ts.net)
  2. They handle TLS termination with a valid certificate
  3. Traffic flows through their edge, then to your device over the encrypted tailnet
  4. Your device never needs to accept inbound connections from the internet

Setting it up

shell
# Expose a local web server on port 8080
tailscale funnel 8080

# Or expose with a specific path
tailscale funnel --set-path=/api localhost:3000

That’s it. You get a working HTTPS URL in seconds.

When to use Funnel vs other options

ScenarioUse FunnelUse Cloudflare TunnelUse port forwarding
Quick demo/webhook testing✅OverkillOverkill
Production with custom domainMaybe✅✅
Self-hosted apps for friends✅✅Pain
Bandwidth-heavy (video streaming)Consider limits✅✅
Full control over edge❌Partial✅

See the Funnel documentation for the full details.

Exit nodes vs Funnel vs Cloudflare Tunnel: which do you need?

These three get confused constantly. Here’s the decision tree:

What are you trying to do?

GoalUseWhy
Route my internet traffic through a trusted locationExit NodeTunnels outbound traffic from your device through another node in your tailnet
Let someone outside my tailnet access a service I’m runningFunnel or Cloudflare TunnelCreates an inbound path from the public internet
Access my own services from my own devicesNeither — just use Tailscale directlyThat’s the default behavior

Funnel vs Cloudflare Tunnel decision:

ConsiderationFunnelCloudflare Tunnel
Setup complexityOne commandMore config, but still reasonable
Custom domainLimited (uses *.ts.net)Full control
DDoS protectionBasicCloudflare’s full stack
Bandwidth limitsYes (free tier)Generous
TLS terminationTailscale’s edgeCloudflare’s edge
Caching/CDNNoYes
Zero-trust access policiesVia Tailscale ACLsVia Cloudflare Access

My recommendation:

MagicDNS + ACLs = sane networking

Two features make Tailscale feel polished:

Example ACL snippet:

JSON
{
  "acls": [
    {"action": "accept", "src": ["group:admins"], "dst": ["tag:server:*"]}
  ]
}

You can also create device tags (like tag:server), then grant access to groups rather than specific devices. That makes permissions easier to reason about as your tailnet grows.

ACL patterns for homelabs

Here’s a real-world ACL structure I use for my homelab. It’s not minimal, but it’s sane:

JSON
{
  "groups": {
    "group:admins": ["user@gmail.com"],
    "group:family": ["spouse@gmail.com", "kid@gmail.com"]
  },
  "tagOwners": {
    "tag:server": ["group:admins"],
    "tag:media": ["group:admins"],
    "tag:nas": ["group:admins"]
  },
  "acls": [
    // Admins can access everything
    {"action": "accept", "src": ["group:admins"], "dst": ["*:*"]},
    
    // Family can access media servers (Jellyfin, etc.)
    {"action": "accept", "src": ["group:family"], "dst": ["tag:media:80,443,8096"]},
    
    // Family can access NAS for file sharing
    {"action": "accept", "src": ["group:family"], "dst": ["tag:nas:445,5000"]},
    
    // Servers can talk to each other (for backups, monitoring, etc.)
    {"action": "accept", "src": ["tag:server"], "dst": ["tag:server:*"]},
    
    // All devices can use the exit node
    {"action": "accept", "src": ["autogroup:members"], "dst": ["autogroup:internet:*"]}
  ]
}

Why this structure works:

Common footguns to avoid:

Real-world use cases

Two power features worth knowing

Performance tips

Tailscale SSH (no more shared SSH keys)

Tailscale can act as an SSH access broker, which means you authenticate with your identity provider instead of copying keys around. It’s particularly nice for teams: access can be revoked instantly without touching server configs.

See Tailscale SSH documentation for setup instructions.

File sharing: Taildrop

Tailscale includes Taildrop, a simple file transfer feature that works inside your tailnet. It’s not Dropbox, but it’s excellent for “get this file to my other device right now.”

If you want the practical walkthrough, here’s the full guide: Self-Hosting With Tailscale

Troubleshooting common Tailscale issues

After running Tailscale for a while, you’ll hit some of these. Here’s how to fix them:

“Connection is relayed” (not direct)

Symptoms: Slow connections, tailscale status shows “relay” instead of “direct”

Diagnosis:

shell
tailscale status
tailscale ping <device-name>
tailscale netcheck

Common causes:

Fixes:

  1. Enable UPnP on your router
  2. Try a different network (mobile hotspot) to isolate the problem
  3. If you control the network, forward UDP 41641 to one device
  4. Accept that DERP is your reality and move on

Device not showing up in tailnet

Symptoms: New device installed Tailscale but doesn’t appear in admin console

Causes:

Fixes:

shell
# Check if tailscale is running
tailscale status

# See which account you're logged into
tailscale status --json | jq .Self.UserID

# Force re-authentication
tailscale logout
tailscale login

Key expiry issues

Symptoms: Device suddenly can’t connect, “key expired” in logs

Cause: By default, device keys expire after 180 days. This is a security feature.

Fixes:

  1. Re-authenticate: tailscale login
  2. Disable key expiry for specific devices in admin console (not recommended for laptops)
  3. Use --authkey with a reusable, non-expiring key for servers (create in admin console)

MagicDNS not resolving

Symptoms: Can ping by Tailscale IP but not by name

Causes:

Fixes for Linux:

shell
# Check if tailscale is handling DNS
resolvectl status

# Force tailscale to take over DNS
sudo tailscale up --accept-dns=true

Subnet router not advertising routes

Symptoms: Subnet router shows as “advertising” but other devices can’t reach the subnet

Causes:

Fixes:

shell
# Enable IP forwarding (Linux)
echo 'net.ipv4.ip_forward = 1' | sudo tee -a /etc/sysctl.conf
echo 'net.ipv6.conf.all.forwarding = 1' | sudo tee -a /etc/sysctl.conf
sudo sysctl -p

# Check routes are approved in admin console
# Then restart tailscale
sudo tailscale up --advertise-routes=192.168.1.0/24 --reset

For more debugging, check the Tailscale troubleshooting guide and the community forum.

When Tailscale is not the right tool

How to evaluate it honestly

Ask yourself:

If you answer “yes” to most of these, Tailscale is a strong fit.

FAQ

Is Tailscale a VPN?

Yes, in the practical sense: your devices sit on a private encrypted network. It’s just not the legacy kind — there’s no server everyone tunnels through, no port to forward, and no shared password. Each pair of devices gets its own WireGuard tunnel.

Can Tailscale see my traffic?

No. Tailscale’s coordination server is a control plane: it checks identity, hands out public keys, and tells devices how to find each other. Your actual traffic goes device-to-device over WireGuard. If even the control plane bothers you, Headscale lets you run that part yourself.

What happens when NAT hole-punching fails?

Traffic falls back to DERP — Tailscale’s relay network. Connections keep working and stay end-to-end encrypted; they just bounce through a relay instead of going direct, so they’re slower. You can see whether a connection is direct or relayed in tailscale status.

Do I need to open any ports?

No. Outbound connections from each device are enough, because the coordination server tells both sides how to reach each other and they punch holes simultaneously. Nothing has to listen on the public internet unless you deliberately use Funnel to expose a service.

Further reading

Summary

Tailscale is a VPN in the most practical sense — it makes your devices feel like they’re on the same private network, without the pain of legacy VPN setups. If your goal is private remote access rather than “hide my browsing,” it’s one of the cleanest solutions available.

Next up: Self-Hosting With Tailscale

Saved you an afternoon? Buy me a tea ↗