Run a Tailscale subnet router on a VPS
Advertise a private network to your tailnet from a VPS: route approval, IP forwarding that survives a reboot, and the --accept-routes flag Linux needs.
What a Tailscale subnet router does
A Tailscale subnet router is one machine that advertises a whole range of private IP addresses to your tailnet, so every device on the tailnet can reach addresses in that range even though nothing there runs Tailscale. Your tailnet is your private Tailscale network: the set of devices signed in to one account or organisation. An exit node is the feature people confuse it with, and it does the opposite job. It sends all of a device's traffic out through the VPS, so the VPS becomes that device's route to the public internet.
One sentence each. A subnet router makes one private network reachable from the tailnet. An exit node changes where your public traffic leaves from. If the second one is what you want, read how to run a Tailscale exit node on a VPS instead. They are separate flags, and one VPS can do both at once, but they solve different problems and they fail in different ways.
When a VPS needs a subnet router
The common case is a private network your provider already gave you. Your VPS has a public address and a second interface on a private segment, and the other servers on that segment have no public address at all: a database at 10.0.0.20, a backup target at 10.0.0.30. Put Tailscale on one VPS, advertise 10.0.0.0/24, and your laptop reaches those private addresses directly. Nothing else on the segment changes, and the database still has no public address.
The other case is a network on the far side of the VPS. A home or office LAN (local area network) behind its own router, or a rack of appliances that cannot run Tailscale at all, such as a managed switch or an old NAS with locked firmware. One Linux box on that network becomes the subnet router for everything else on it.
Both cases share one requirement. The subnet router must already be able to reach the range it advertises, using its own routing table and its own firewall. Tailscale does not build that connection. It carries traffic to the router and hands it to the kernel to forward.
Install Tailscale and check the local route first
curl -fsSL https://tailscale.com/install.sh | shThe script detects the distribution, adds Tailscale's package repository, installs the tailscale command and the tailscaled daemon, then enables the service. Confirm it with systemctl is-active tailscaled, which should print active.
Before anything else, prove the VPS can reach the network you plan to advertise.
ip route show
ping -c3 10.0.0.20ip route show must list the private range on a real interface, something like 10.0.0.0/24 dev enp7s0 proto kernel scope link src 10.0.0.5. If the ping fails here, on the router itself, no Tailscale flag will fix it. The problem is the VPS network configuration or a firewall on the target host. Fix that first, because every later test depends on it.
Turn on IP forwarding, and make it survive a reboot
A Linux machine drops any packet that is not addressed to itself unless forwarding is on. Forwarding other machines' packets is the entire job of a subnet router, so this step is not optional.
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.confCheck it with sysctl net.ipv4.ip_forward, which should print net.ipv4.ip_forward = 1.
People often get this step half right. sudo sysctl -w net.ipv4.ip_forward=1 works immediately and is gone at the next boot, so the subnet router runs for weeks and then stops the morning after a kernel upgrade reboot. The confusing part is that nothing looks broken. tailscale status still shows the node online, the admin console still shows the route approved, and clients still have the route installed. Packets arrive at the VPS and the kernel drops them without logging anything. Writing the values into /etc/sysctl.d/99-tailscale.conf is what brings them back after a reboot.
If you advertise routes with forwarding still off, tailscale up warns you at the time, with a line close to Warning: IPv4 forwarding is disabled. Subnet routes and exit nodes may not work correctly. Read the output of that command instead of scrolling past it.
Advertise the routes
sudo tailscale up --advertise-routes=10.0.0.0/24On a VPS that is already signed in to your tailnet, change the setting in place instead:
sudo tailscale set --advertise-routes=10.0.0.0/24Use tailscale set for every later change. Re-running tailscale up with a single flag resets the flags you did not repeat, and the CLI stops you with an error saying that changing settings this way requires mentioning all non-default flags. tailscale set changes one setting and leaves the rest alone.
Several ranges go in one comma separated list with no spaces: --advertise-routes=10.0.0.0/24,192.168.50.0/24. Each entry must be a network address in CIDR notation (classless inter-domain routing, the 10.0.0.0/24 form). Writing your own host address by mistake, 10.0.0.5/24, is rejected because the bits after the prefix are not zero, and the error names the prefix you probably meant. To stop advertising, set an empty list with sudo tailscale set --advertise-routes=.
Approve the route in the admin console
Advertising a route is a request, not a change. Until an admin approves it, no client receives the route and nothing in the range is reachable. This is deliberate, because a machine that can add itself to everyone's routing table can capture traffic for any range it likes.
Approve it on the Machines page of the admin console. The VPS is listed with a subnet badge. Open its row, find the subnets section, edit the route settings, tick the route, and save.
Approval is per prefix. Advertise 10.0.0.0/24 today and 192.168.50.0/24 next month, and the new prefix arrives unapproved while the old one keeps working. An approved route and an ignored route look identical from the VPS, so check the console before you debug anything else.
You can skip the manual step with an autoApprovers block in the tailnet policy file:
{
"autoApprovers": {
"routes": {
"10.0.0.0/24": ["tag:subnet-router"]
}
}
}Then bring the node up with that tag, sudo tailscale up --advertise-routes=10.0.0.0/24 --advertise-tags=tag:subnet-router, and the route is approved the moment it is advertised. The tag has to exist in the tagOwners section of the same policy file first. This is worth setting up if you rebuild the VPS from a script, because a rebuilt node is a new node and its routes start unapproved again.
Why Linux clients ignore the route without --accept-routes
The route is now advertised and approved. Your phone and your Mac can reach 10.0.0.20. Your Linux laptop cannot, and nothing in the admin console suggests a problem.
Accepting a subnet route means writing entries into the client's routing table. On Android, iOS, macOS, tvOS and Windows, the Tailscale client does that for you. On Linux it does not, because a Linux machine is often a server or a router whose routing table someone configured on purpose, and silently inserting a /24 learned from the network could break traffic that machine already handles. So on Linux you opt in, on each client:
sudo tailscale set --accept-routesThen check where the route landed:
ip route show table 52
ip route get 10.0.0.20Tailscale on Linux does not put accepted routes in the main routing table. It puts them in routing table 52 and installs policy rules, visible with ip rule show in the priority range 5210 to 5270, that send unmatched packets to that table. So ip route show on its own will never list 10.0.0.0/24, and a reader who checks only that command concludes that --accept-routes did nothing. ip route show table 52 is the command that shows the truth, and it should list the advertised range on tailscale0.
One exception is worth knowing. If this Linux node is itself a second subnet router for its own local network, --accept-routes makes it send traffic for its own directly connected subnet through the other router instead of out its own interface. On a standby router in a high availability pair, leave --accept-routes off and advertise only.
Failure mode: two routers advertising overlapping ranges
Two subnet routers must not advertise identical ranges. Overlapping ranges with different prefix lengths are allowed, and Tailscale picks the most specific match. With router A advertising 10.0.0.0/24 and router B advertising 10.0.0.0/16, traffic to 10.0.0.20 goes to A.
What surprises people is the behaviour when A goes offline. Tailscale does not fall back to the less specific route. Traffic to 10.0.0.20 stops, while traffic to 10.1.0.20 keeps working through B. The symptom looks like half of the private network being down, and the cause is one offline node holding the more specific prefix. If you want failover, have the wider router also advertise the narrower prefixes, so both cover the same addresses.
The other overlap is closer to the client. Sitting on a hotel network at 192.168.1.0/24 while your subnet router advertises 192.168.1.0/24 means the two compete for the same destinations, and which one wins depends on the platform. On Linux, install a rule ahead of Tailscale's own so local addresses use the main table:
sudo ip rule add to 192.168.1.0/24 priority 2500 lookup mainThat rule is not persistent and is gone at the next boot. The real fix is choosing a private range you will not meet in the wild. 192.168.0.0/24 and 192.168.1.0/24 are the defaults on most home routers, so pick something inside 10.0.0.0/8 that you chose deliberately. The same collision breaks a plain WireGuard VPN you configure by hand, for the same reason: the more specific local route wins, so the traffic never enters the tunnel.
Failure mode: DNS resolves to an address no route covers
This one is hard to debug, because nothing reports an error. The name resolves. The connection times out.
Say db.internal.example.com resolves to 10.0.5.20 through your private nameserver, and you advertised 10.0.0.0/24. The lookup succeeds, because DNS (domain name system) resolution and IP routing are separate steps and neither one checks the other. Then the packet to 10.0.5.20 finds no matching route on the tailnet, so it leaves through the client's default gateway and disappears.
Two commands separate the two halves:
nslookup db.internal.example.com
ip route get 10.0.5.20If the lookup returns an address but ip route get does not answer with dev tailscale0, the name is fine and the route is missing. Advertise a range that covers the address, either 10.0.0.0/16 or a second explicit prefix, then approve the new prefix in the console.
There is a matching trap on the nameserver itself. If you set a global nameserver in the admin console at a private address such as 10.0.0.53, that address must sit inside an approved route, or your devices cannot reach the resolver at all. Turn on the option that overrides local DNS servers while pointing at a resolver nobody can reach, and every device in the tailnet loses name resolution at once, including the ones that worked a second earlier. Advertise and approve the route to the resolver first, then change the DNS setting. If DNS inside a tunnel is the part you keep fighting, the way DNS breaks over a WireGuard tunnel covers the same mechanism without the coordination layer on top.
Source NAT, and site to site links
By default the subnet router rewrites the source address of every forwarded packet to its own private address. That is SNAT (source network address translation), and it exists so replies work without changing anything on the private network: the database at 10.0.0.20 answers the VPS, which it already knows how to reach. The cost is that the database sees every tailnet connection as coming from the VPS, so per source firewall rules and access logs tell you nothing.
Turn it off on Linux when you want the client's real tailnet address preserved:
sudo tailscale set --snat-subnet-routes=falseThe hosts on the private network then need a route back to 100.64.0.0/10, the range Tailscale assigns to devices, pointing at the subnet router. Without that return route their replies go to the default gateway and never arrive, so connections hang after the first packet. Add the static route on the private network's gateway, or leave SNAT on.
A site to site link is two subnet routers doing this at once, each advertising its own network and accepting the other's:
sudo tailscale up --advertise-routes=10.0.0.0/24 --snat-subnet-routes=false --accept-routesRun the matching command on the other router with its own range. The two ranges must be different. If large transfers stall while ssh and ping are fine, the cause is MSS (maximum segment size), the largest chunk of data a TCP packet carries. The tunnel's overhead makes forwarded packets too big for some link in the middle, and clamping fixes it:
sudo iptables -t mangle -A FORWARD -o tailscale0 -p tcp -m tcp --tcp-flags SYN,RST SYN -j TCPMSS --clamp-mss-to-pmtuSave that rule with iptables-persistent, or it disappears at the next boot.
Housekeeping that keeps it running
Node keys expire after 180 days by default, as of August 2026. When the key on a subnet router expires, the node signs out and the whole advertised range becomes unreachable, with no configuration change anywhere to explain it. Disable key expiry for this machine on the Machines page of the admin console, then write down that you did.
Tailscale prefers a direct connection between peers and falls back to its relay servers when it cannot get one. The relays work, and they add latency. A VPS with a public address is the easy case: allow inbound UDP 41641 and most peers connect directly. If ufw is managing the firewall, the ufw rules a VPS actually needs covers the syntax.
Access rules are the other half. On a default tailnet every device of yours can reach every other one, so an approved route just works. Once you write an ACL policy, the destination side of a rule has to name the private range, because 10.0.0.20 is not a tailnet address and is not covered by rules written against tailnet IPs or tags.
Finally, decide whether you want a coordination server you do not run. Tailscale's control plane is a hosted service. Your keys stay on your machines, but the account and the policy file live there. Running Headscale, the self hosted Tailscale control server keeps that on your own VPS, at the cost of maintaining it. If you are still deciding between this model and hand written config, the comparison of WireGuard and Tailscale explains what the coordination layer gives you and what it costs.
FAQ
What is the difference between a subnet router and an exit node?
A subnet router advertises a range of private addresses, so tailnet devices can reach machines that do not run Tailscale. An exit node advertises itself as a route to the whole internet, so a device sends all of its traffic out through that node's public address. One VPS can be both. They are separate flags, --advertise-routes and --advertise-exit-node, and each needs its own approval in the admin console.
Why does my Linux client ignore the advertised subnet route?
Linux clients do not accept subnet routes unless you ask them to. Run sudo tailscale set --accept-routes on the client. Then check with ip route show table 52, not ip route show. Tailscale installs accepted routes in routing table 52 and reaches them through policy rules, so the main table never lists them and a working route looks missing.
My subnet stopped working after a reboot. What broke?
IP forwarding, most likely. A value set with sysctl -w does not survive a reboot, so write it to /etc/sysctl.d/99-tailscale.conf and confirm with sysctl net.ipv4.ip_forward. If forwarding is on and the range is still unreachable, look at the node in the admin console. Node keys expire after 180 days by default, and an expired subnet router looks like a network fault rather than an account problem.
Can two subnet routers advertise the same range?
Not identical ranges. Overlapping ranges with different prefix lengths are fine, and the most specific one wins. Failover needs care: when the router holding the more specific prefix goes offline, Tailscale does not fall back to the wider route, so that traffic stops. For a real standby pair, have both routers advertise the same specific prefixes.
The hostname resolves but the connection times out. Why?
DNS resolution and routing are separate steps. A name can resolve to an address that no approved route covers, and the packet then leaves through the client's default gateway. Run ip route get <address> on the client. If the answer does not include dev tailscale0, advertise a range that covers that address and approve the new prefix in the admin console.