VPN 📖 14 min read

Tailscale Mesh VPN

Tailscale builds a private network between your devices on top of WireGuard, without opening ports on your router. This guide installs it on Linux, macOS, Windows and phones, then covers MagicDNS, exit nodes, subnet routers, Tailscale SSH, access rules and device sharing, and the self-hosted Headscale control server. It ends with a neutral look at how Tailscale compares with ZeroTier, Netmaker and plain WireGuard.

What You Need

  • Hardware: At least two devices to connect, for example a laptop, a phone and a Linux server. For a subnet router or exit node, a Linux machine that stays switched on.
  • Versions: The commands follow Tailscale's CLI reference (last validated July 30, 2026) and the 1.104 client series, the newest stable release on October 20, 2026. Flags are occasionally renamed, so check tailscale version and the CLI reference if a command is rejected.
  • Software: A current Tailscale client for each device (Linux, macOS, Windows, iOS or Android) and an account with one of the identity providers Tailscale supports for sign-in. For Headscale: a Linux server with Docker, a domain name and a reverse proxy with HTTPS.
  • Time: Under an hour for the first devices, an exit node and a subnet router; allow extra time for Headscale

What Makes Tailscale Different

Each device runs the Tailscale client, which builds WireGuard tunnels directly to the other devices it talks to. A coordination server, hosted by Tailscale, hands out public keys and your access rules; it doesn't carry your traffic. According to Tailscale's DERP servers documentation, most connections end up direct. When NAT or a firewall prevents that, traffic falls back to a relay: a peer relay in your own tailnet if you've set one up, otherwise one of Tailscale's DERP servers. Relayed traffic is still WireGuard-encrypted, and private keys never leave your devices.

That design is what lets two devices behind home routers, or behind carrier-grade NAT, reach each other without port forwarding. Plain WireGuard needs at least one side with a reachable UDP port, as described in WireGuard Cloud Deployment.

How it compares with similar tools, based on each project's documentation:

Aspect Tailscale ZeroTier Plain WireGuard
Client Tailscale client, WireGuard tunnels ZeroTier One client wg and wg-quick, or the WireGuard apps
Control plane Hosted by Tailscale; Headscale is an open-source self-hosted alternative Hosted ZeroTier Central, or a self-hosted network controller None; each peer's config file is the source of truth
Keys and peers Distributed by the coordination server Managed through the network controller Generated and copied to each peer by hand
Default routing Only tailnet traffic; internet traffic only via an exit node Split tunnel by default Whatever the client's AllowedIPs say

Netmaker is another WireGuard-based option. Its documentation describes a Netmaker server that you install and run yourself.

Installation

Tailscale's download page has installers for every platform.

Linux

The install script detects your distribution (Ubuntu, Debian, Fedora, RHEL, Raspberry Pi OS, Amazon Linux and others listed on the Linux install page) and adds Tailscale's package repository:

curl -fsSL https://tailscale.com/install.sh | sh

If you'd rather not pipe a script into a shell, the same page links to per-distribution instructions that add the repository by hand.

macOS

Install the app from the Mac App Store or the standalone package on the download page. Tailscale publishes more than one macOS variant; only the open-source tailscaled command-line build can act as a Tailscale SSH server.

Windows

Run the installer from the download page. The client sits in the system tray, where you sign in and change settings.

iOS and Android

Install Tailscale from the App Store or Google Play and sign in. Both apps can use an exit node. Android can also act as one, but Tailscale's exit node docs say Android exit nodes run in userspace and may be too slow for most uses.

Connect Your First Device

sudo tailscale up

On Linux this prints a URL. Open it, sign in, and the device joins your tailnet; it then shows up on the Machines page of the admin console. Repeat on each device with the same account. Devices find each other through the coordination server, so there's no peer list to maintain.

Devices re-authenticate periodically when their key expires. For servers and other always-on machines you can disable key expiry for that device on the Machines page; the Linux install page warns that this reduces security, so do it only for devices you trust.

If tailscale up fails on Linux, check the daemon with sudo systemctl status tailscaled and sudo journalctl -u tailscaled -b. The install page also notes that Tailscale needs the tun kernel module.

Using Tailscale

Each device gets a Tailscale IPv4 address from 100.64.0.0/10 plus an IPv6 address, and Tailscale's DNS docs say these stay the same whichever network the device is on. List devices and show this machine's IPv4 address with:

Terminal window with the commands from the section Using Tailscale
Reach a machine by its tailnet address, or by name with MagicDNS.
tailscale status
tailscale ip -4

MagicDNS, which is on by default, gives every device a name, so you can use the machine name instead of the address:

ssh user@100.x.y.z
# or, with MagicDNS
ssh user@laptop

The DNS page of the admin console also sets global nameservers and split DNS for internal domains (see DNS in Tailscale). If you run Pi-hole or AdGuard Home on a tailnet device, you can add its Tailscale IP as a nameserver so filtering follows your devices.

Exit Nodes

By default Tailscale only carries traffic between tailnet devices, and your normal internet traffic goes out as usual. An exit node routes a device's internet traffic through another device in your tailnet, which helps on untrusted Wi-Fi. The exit node docs require three opt-ins: the device advertises itself, an admin allows it, and each client chooses to use it.

Terminal window with the commands from this section
Route all traffic through an exit node; an empty value stops using it.

On a Linux exit node, enable IP forwarding first (the same commands as in the subnet router section below), then advertise it:

sudo tailscale set --advertise-exit-node

Allow it on the Machines page: open the device's menu, choose Edit route settings and enable Use as exit node. Then select it on a client. On Linux, according to the CLI reference:

sudo tailscale set --exit-node=<exit-node-name-or-IP>
# stop using an exit node (empty value)
sudo tailscale set --exit-node=

While connected to an exit node, a device can't reach its own local network unless you allow it with --exit-node-allow-lan-access (or the Allow LAN access toggle in the apps). If you've replaced the default access policy, using an exit node also needs a rule whose destination is autogroup:internet.

With plain WireGuard the same result means setting AllowedIPs = 0.0.0.0/0 on the client and configuring forwarding and NAT on the server yourself.

Subnet Routers

A subnet router gives tailnet devices access to a whole network, including devices that can't run Tailscale, such as printers or a NAS. On a Linux machine in that network, enable IP forwarding:

Terminal window with the commands from the section Subnet Routers
Turn on IP forwarding so the machine can act as a subnet router.
echo 'net.ipv4.ip_forward = 1' | sudo tee -a /etc/sysctl.d/99-tailscale.conf
echo 'net.ipv6.conf.all.forwarding = 1' | sudo tee -a /etc/sysctl.d/99-tailscale.conf
sudo sysctl -p /etc/sysctl.d/99-tailscale.conf

Then advertise the routes, replacing the example subnet with yours:

sudo tailscale set --advertise-routes=192.168.1.0/24

Approve the routes in the device's Edit route settings panel on the Machines page. Windows, macOS, iOS and Android clients accept approved routes automatically; other Linux machines need sudo tailscale set --accept-routes. The subnet router docs recommend that the router's firewall denies forwarding by default (ufw and firewalld do), so only the traffic you intend gets routed.

Subnet routers use source NAT by default, so devices on the LAN see connections coming from the router itself. If subnets and NAT are new to you, Networking Fundamentals explains both.

Tailscale SSH

Tailscale SSH lets the tailnet handle SSH authentication: instead of managing authorized_keys, your tailnet policy decides who may log in. The server side runs on Linux (and the open-source macOS CLI build). On the host, run:

sudo tailscale set --ssh

Then connect with a normal SSH client to the machine's MagicDNS name or Tailscale IP, for example ssh ubuntu@server. The Tailscale SSH docs point out a few things to know first:

  • Running tailscale set --ssh makes existing SSH sessions to the host's Tailscale IP hang, so run it over the LAN or public address, or from a console.
  • The default policy lets each user reach their own devices as a non-root user. If you've edited the policy, a connection needs both a network rule and an ssh rule.
  • Only connections to the Tailscale IP are taken over. Your regular sshd configuration and keys are left alone, so they still need hardening; see Locking Down SSH.

Access Control

Access rules live in the tailnet policy file, which you edit on the Access controls page of the admin console. The default policy lets every device reach every other device. For new policies Tailscale recommends grants; older acls rules keep working but won't get new features (see the policy syntax reference). A small example that lets an admin group reach tagged servers over SSH and HTTPS, and other members only over HTTPS:

{
  "groups": {
    "group:admins": ["alice@example.com"]
  },
  "tagOwners": {
    "tag:server": ["group:admins"]
  },
  "grants": [
    {"src": ["group:admins"], "dst": ["tag:server"], "ip": ["tcp:22", "tcp:443"]},
    {"src": ["autogroup:member"], "dst": ["tag:server"], "ip": ["tcp:443"]}
  ]
}

Replacing the default policy also removes what it allowed, including the default SSH rule and exit node access, so add those back if you use them. The policy file can also contain tests that assert who can and can't reach what, which catches mistakes before they lock someone out.

Sharing a Device

To give someone outside your tailnet access to a single machine, open its menu on the Machines page and choose Share, then send an email invite or copy an invite link. The recipient gets that machine and nothing else, and shared machines are quarantined: they accept connections from the other tailnet but can't start them. Remove the share when the access is no longer needed. The sharing docs currently label the feature as beta.

Self-Hosted Alternative: Headscale

With Tailscale's hosted service your traffic is end-to-end encrypted, but key distribution and policy run on Tailscale's coordination server. Headscale is an open-source implementation of that control server that you run yourself and use with the regular Tailscale clients. It doesn't cover every feature of the hosted service, and it has no built-in web interface (community projects provide several).

Versions: the commands follow Headscale's container documentation, which the project marks as community-maintained, and pin 0.29.6 (a security and patch release, October 24, 2026), the newest release on the Headscale releases page when this guide was updated. Those release notes list Tailscale v1.80.0 as the minimum supported client. Read the release notes before changing the tag.

mkdir -p ./headscale/{config,lib}
cd ./headscale
# Save the example config for your Headscale version as ./config/config.yaml and edit it first

cat > compose.yaml <<'YAML'
services:
  headscale:
    image: docker.io/headscale/headscale:0.29.6
    container_name: headscale
    command: serve
    read_only: true
    tmpfs:
      - /var/run/headscale
    volumes:
      - ./config:/etc/headscale:ro
      - ./lib:/var/lib/headscale
    ports:
      - "127.0.0.1:8080:8080"
      - "127.0.0.1:9090:9090"
    healthcheck:
      test: ["CMD", "headscale", "health"]
      interval: 30s
      timeout: 10s
      retries: 3
    restart: unless-stopped
YAML
docker compose config --quiet
docker compose up -d
docker inspect --format '{{.State.Health.Status}}' headscale

curl http://127.0.0.1:8080/health

The health check uses Compose’s exec-array form: the normal image is distroless and has no shell, so a shell-form --health-cmd is unsuitable. Wait for the health status to become healthy and inspect container logs if it does not. Here the container only listens on localhost. Clients must reach Headscale from the internet over HTTPS at the server_url set in its config, so put a reverse proxy with a certificate in front of it (the Nginx Web Server Setup guide covers that). Then create a user and register a device:

docker exec -it headscale headscale users create alice

# on the client
sudo tailscale up --login-server https://headscale.example.com

# on the server, with the auth ID shown on the login page
docker exec -it headscale headscale auth register --user alice --auth-id <AUTH_ID>

Running your own control server makes you responsible for its uptime, for backing up ./lib (where the database lives) and for upgrades.

Troubleshooting

Tailscale's own troubleshooting index splits problems into connectivity, DNS, operating-system and container topics. The checks below cover the ones this guide's setup most often runs into. Commands and flags are from Tailscale's CLI reference.

Terminal window with the commands from the section Troubleshooting
When a connection is slow, tailscale ping shows whether it's direct or relayed.

A device is listed but you can't reach it

Start with tailscale status. For an active peer it shows direct followed by an address when the connection is peer to peer, and relay followed by a city code such as nyc or fra when traffic goes through one of Tailscale's DERP relay servers. Relayed traffic still works, but it's usually slower. To test one path, run tailscale ping with the device's name or 100.x.y.z address; adding --until-direct keeps pinging until a direct path is established (use --timeout to bound how long it waits).

tailscale status
tailscale ping laptop
tailscale ping --until-direct laptop

If a connection stays relayed, tailscale netcheck reports your physical network's conditions, including UDP support, IPv4 and IPv6, NAT behaviour, port mapping and latency to each relay region. A network that blocks UDP, or a strict NAT on both sides, is the usual reason a path can't go direct.

tailscale netcheck

Check: after you fix the cause, tailscale status shows direct for the peer, or tailscale ping answers.

Linux: tailscale up fails or the daemon isn't running

Use sudo systemctl status tailscaled and sudo journalctl -u tailscaled -b, as shown in the connection section above. The Linux section of Tailscale's troubleshooting docs also lists three specific cases: DNS problems, a state file that can't be unsealed, and a missing operator permission for the daemon.

Subnet routes don't work

  • The advertised routes must be approved on the Machines page, unless an autoApprovers rule in your policy already covers them. Advertising a route does nothing until it's approved.
  • IP forwarding must be enabled on the Linux subnet router, with the sysctl lines from the subnet router section above.
  • Other Linux machines only use approved routes after sudo tailscale set --accept-routes. Windows, macOS, iOS and Android accept them automatically.
  • On a firewalld host, Tailscale's subnet documentation notes a known issue that may need firewall-cmd --permanent --add-masquerade.
  • Route approval and access rules are separate: a route can be approved and still blocked by your access policy.

An exit node doesn't carry your traffic

Check that the exit node advertises itself, that IP forwarding is on, and that you selected it on the client with --exit-node. While an exit node is in use the device can't reach its own LAN unless you add --exit-node-allow-lan-access, as described in the exit node section.

Names don't resolve

Check that MagicDNS is on in the admin console's DNS page and try the Tailscale 100.x.y.z address directly. If the address works and the name doesn't, the problem is DNS, not connectivity. On Linux, Tailscale has a dedicated page for DNS issues, linked from its troubleshooting index.

A device drops off the tailnet

Keys expire. A device whose key has expired has to sign in again with sudo tailscale up. For servers, disable key expiry on the Machines page, as described in the first-device section.

Plans and Limits

Tailscale's pricing page lists a free Personal plan for non-commercial use with up to 6 users and unlimited user devices, and lists subnet routers, exit nodes and MagicDNS on every plan. Tailnets created with a custom (work) email domain are treated as business use and start on a trial. Plans change, so check the pricing page before relying on a specific limit.

Which One Fits?

Tailscale (or Headscale) suits you when:

  • Some devices sit behind NAT you don't control, including carrier-grade NAT, so you can't open an inbound port.
  • You connect many devices to each other, not just to one server.
  • You want device names through MagicDNS, per-user access rules and device sharing without editing config files on every peer.

Plain WireGuard suits you when:

  • You don't want a third-party coordination service, and running Headscale isn't worth it for your setup.
  • You have a server with a reachable UDP port and a handful of clients in a hub-and-spoke layout. WireGuard Cloud Deployment covers that by hand, and WireGuard-Easy Deployment adds a web UI for managing clients.

ZeroTier and Netmaker solve the same problem with different designs. Compare the ZeroTier documentation and the Netmaker documentation linked above against the features you need.