
From one desktop, every host on the local network went dark. SSH timed out. The NAS push hung. A local MCP server that talks to a vault over SSH stopped answering. The gateway still pinged, so the obvious conclusion was that the homelab had fallen over — and we spent the first stretch of the evening checking hypervisors that were, in fact, fine.
They were fine because the network was fine. A host on a different VLAN could reach every one of the "dark" machines. The failure belonged to exactly one desktop, and the cause was that it had been told to route the LAN it was physically plugged into through a VPN tunnel, out to a subnet router, and back. That hairpin had been working, invisibly, for a while. Then two more faults underneath it broke it outright, and the result read as a total outage from a machine that was sitting on the same switch as everything it could not reach.
Key Takeaways
- A Tailscale subnet route and an Ethernet route for the same subnet both exist on a machine that has
accept-routesenabled, and the OS picks the Tailscale route because its metric is lower — on Windows, 0 against the Ethernet adapter's 256 - An on-LAN machine with
accept-routesenabled hairpins every LAN packet through a subnet router that may be three hops away, which works until something in that path breaks, and then looks like the entire LAN is down - The signature is specific: the gateway pings,
tailscale pingto the router succeeds and reports a direct LAN endpoint, and nothing else on the subnet answers - Two Tailscale daemons on one machine — a Windows host and WSL2 in mirrored networking mode — both bind UDP 41641 on the same addresses, so direct tunnel packets land on the wrong daemon; the fix is a distinct port for the second daemon
- A dual-port NIC with both ports on the same /24 makes the machine's tunnel endpoint flap between two addresses, hundreds of times an hour in the router's log; one port per subnet, pinned with a DHCP reservation
accept-routesbelongs on roaming devices only — the laptop, the tablet, the phone — and never on a machine that lives on the LAN it would be routing to- A cross-VLAN reachability check is the fastest way to separate "the network is down" from "this machine's view of the network is down", and it should come before any power cycle
What Subnet Routing Is Doing When It Works
For remote access to a home or office network, Tailscale's subnet routing is the simplest thing that works. One machine on the LAN advertises the local subnet to the tailnet. The admin console approves the route. Every tailnet device that enables accept-routes then sees that subnet as reachable through the tunnel, and SSH aliases, hypervisor UIs, NAS web interfaces, and internal dashboards all work from a coffee shop exactly as they do at home. We run two subnet routers for the same /24 on two different hypervisor hosts, and a failover drill in which one went down for about thirty seconds dropped zero probes.
The mechanism is a route. On the accepting device, the subnet appears in the routing table with the Tailscale interface as the next hop. That is the entire feature, and it is also the entire problem, because a route does not know where the device physically is.
The Hairpin That Was Already There
Our desktop lives on the LAN. It has a wired connection to the same subnet the subnet routers advertise. It also had accept-routes enabled, left over from a period when it was being used remotely.
That gave the OS two routes to the same /24: one through the Ethernet adapter, and one through Tailscale. Windows chose by metric. The Tailscale route carried metric 0; the Ethernet route carried the adapter's default of 256. Lower wins. Every packet to a LAN host went into the tunnel, across to whichever subnet router was active, out of that router's LAN interface, to the destination, and back along the same path.
The desktop could reach its own subnet only by way of a machine on that subnet. It had been doing this for some time without anyone noticing, because a hairpin through a healthy router on a fast LAN is slower but not broken.
On Windows, the question "which interface will this packet leave on" has a direct answer:
Find-NetRoute -RemoteIPAddress <lan host>
If the interface alias that comes back is Tailscale for an address on your own wired subnet, you are hairpinning. On macOS the equivalent is route get <lan host>, and the tell is a utun interface rather than en0.
Why It Stopped Working
A hairpin through a healthy router keeps working. Ours stopped, and the first wrong theory was that the subnet router had failed. It had not; its tunnel state, its route advertisement, and its reachability from other tailnet devices were all verified healthy. The break was local to the desktop, in two layers.
Two daemons on one port. The desktop runs Tailscale on the Windows host. It also runs WSL2, and that Linux environment had its own tailscaled, enrolled as its own node, with the default PORT=41641 in its service configuration. On its own, that is fine — two network stacks, two bindings. But WSL2 was in mirrored networking mode, which gives the Linux side the same IP addresses as the Windows host. Two daemons, the same addresses, the same UDP port. When the subnet router sent direct-LAN tunnel packets to the desktop's endpoint, they reached whichever daemon the stack handed them to, and that was not reliably the daemon that owned the session.
The fix is one line in the WSL daemon's defaults:
PORT="41642"
A second Tailscale daemon on the same machine gets its own port. There is no version of sharing one that behaves.
Two NIC ports on one subnet. The subnet router's log told the second half of the story. The desktop's endpoint address was flapping between two addresses on the LAN — 204 times in an hour. The desktop has a dual-port 10 gigabit NIC, and both ports were cabled and addressed on the same /24, one at 10G and one at 1G. The tunnel's view of "where this peer is" changed every time the OS chose a different source port for outbound traffic, and a peer that moves 204 times an hour does not hold a stable direct connection.
We disabled the second port outright and gave the remaining one a DHCP fixed-address reservation in the gateway, so the desktop has exactly one address on that subnet and it does not change. A few stale references to the retired address — an SSH config entry, a backup ingress rule on another host — were cleaned up the same day, which is its own small lesson: an address that was never meant to be a second identity had quietly become one.
The Signature, So You Recognize It Next Time
Three observations together identify this failure and nothing else we have hit:
- The default gateway answers ping. The gateway is on the LAN, but it is also the one destination a hairpinning machine can still reach, because the Tailscale subnet route typically does not swallow traffic to the local router the way it swallows traffic to hosts.
tailscale ping <subnet router>succeeds, and the output says it reached the router via a direct LAN address on port 41641 — the tunnel itself is up.- Nothing else on the LAN answers. Not the NAS, not the hypervisors, not a web UI on a container, not a service you can reach from any other machine in the building.
The second item is the one that misleads. It reads as "Tailscale is healthy", and it is. The tunnel is up and the hairpin is live. The problem is that the hairpin exists at all, and what sits inside it.
The confirming test is a host on a different VLAN, or any other machine on the same LAN. If it reaches everything the first machine cannot, the network is fine and the first machine's routing is the fault. That check takes thirty seconds and should precede touching a server.
Accept Routes Only Where You Roam
The fix for the hairpin itself is one command on the desktop:
tailscale set --accept-routes=false
After that, the LAN route through Tailscale disappears from the routing table and the Ethernet route is the only one left. Every host on the subnet answered immediately.
The rule that came out of it is worth writing down as policy rather than leaving as a one-time fix. A machine that is physically on the LAN must never route LAN traffic through Tailscale. accept-routes is a roaming-device setting. It belongs on the laptop that leaves the building, the tablet, the phone. It does not belong on a desktop, a workstation, a hypervisor, or anything else that has a cable into the subnet it would otherwise be routing to.
The WSL node on the same desktop had accept-routes off already, which is a reminder that the setting is per node, not per machine. A machine with two enrolled nodes needs the rule applied to both.
The Second-Order Lesson: Three Faults, One Symptom
Nothing about this incident was a single bug. It was a hairpin that had been tolerable, plus a port collision that made the hairpin's tunnel unreliable, plus an endpoint flap that made it worse. Any one of those alone might have gone unnoticed for months. Together they produced a total outage from one chair, and the first hour of diagnosis went to the hypervisors because "every LAN host is down" is what a hypervisor failure looks like.
The order that worked was: confirm the network from somewhere else, then read the routing table on the affected machine, then read the subnet router's log for that machine's peer. Each step narrowed the fault domain by a layer. The subnet router's log in particular is underused — it records where it believes each peer is and how often that changes, and 204 endpoint changes an hour is a number that explains itself.
There is a security angle to this beyond availability. A machine that hairpins its LAN traffic through a subnet router is sending every local packet — SSH sessions, NAS writes, management-UI logins — through a path nobody designed, logged, or reviewed. The router was ours and the LAN was ours, so the exposure was nil. The habit of knowing which interface each class of traffic leaves on is not.
Honest Limits
This is a Windows-and-WSL2 incident. The route-metric behavior — Tailscale at 0, Ethernet at 256 — is Windows-specific. macOS and Linux have their own metric and priority rules, and a hairpin on those platforms will exist for the same reason but may surface differently. The route get check on macOS is the equivalent diagnostic; we have not had to run this down on a Linux desktop.
The UDP 41641 collision is specific to running two Tailscale daemons on one machine with shared addresses. Default WSL2 networking uses NAT and separate addresses, and does not collide. Mirrored mode is what made it a problem.
We have not measured what the hairpin cost in throughput or latency while it was "working". It was slower; we did not benchmark it, and any figure here would be invented.
Finally, none of this argues against subnet routing. It is the right tool for remote access to a small network, and both of our subnet routers were healthy throughout. The fault was a setting on a device that did not need it.
Where This Fits
A remote-access overlay changes a machine's routing table, and a routing table does not know where the machine is. That is the whole incident, and the general shape — a control working exactly as configured and producing a failure that looks like something else entirely — is the same one we hit when our own DNS blocklist silently blocked a vendor's API family. In both cases the network was doing what it was told, and the diagnosis began with checking the network from a second vantage point before trusting the first one.
Designing and operating the remote-access layer — subnet routers, failover between them, which devices accept which routes, and the DHCP and port hygiene underneath — is security infrastructure work, and keeping the hosts behind it reachable is managed infrastructure.
If you run a VPN overlay across devices that are sometimes on the LAN and sometimes not, and nobody has written down which ones are allowed to accept routes, book a call and we will go through the routing tables with you.