Skip to content

Instantly share code, notes, and snippets.

@dsnet
Last active December 9, 2025 18:40
Show Gist options
  • Select an option

  • Save dsnet/b0a602b15651e9502b9d8c5601053bb9 to your computer and use it in GitHub Desktop.

Select an option

Save dsnet/b0a602b15651e9502b9d8c5601053bb9 to your computer and use it in GitHub Desktop.
Merging Tailscale networks

Introduction

Suppose you have both a personal tailnet and a corporate tailnet. As of 2023-10-01, there is no easy way to use both tailnets simultaneously. At best, Tailscale now supports fast user switching, but that's still not quite as convenient as simultaneous use of multiple tailnets. For reasons that won't be discussed here, there are technical challenges with natively merging tailnets within Tailscale.

This article explores manually merging multiple tailnets under the following conditions:

  1. There is a "primary" tailnet, where Tailscale operates as it normally should.
  2. There is (one or more) "secondary" tailnets, where Tailscale mainly works for HTTP-based protocols through the browser.

Approach

The idea for bridging two tailnets is to have a machine that is part of both tailnets that acts a proxy for relaying traffic to the appropriate tailnet.

For the "primary" tailnet, Tailscale is running at root with kernel-level tunneling support (i.e., as Tailscale is usually run). This allows networking to operate as we would expect it to for the "primary" tailnet.

For the "secondary" tailnet, Tailscale is running in userspace (possibly as non-root) with userspace networking support and exposing a SOCKS5 proxy. Technically, a SOCKS5 proxy can support networking traffic beyond just HTTP, but the challenge is the lack of transparent support for SOCKS5 across all networking applications.

As an example, let's assume the following initial setup:

  • We have a single Linux machine we'll use at the proxy.
  • Tailscale is already installed at root and logged into the "primary" tailnet.
    • Let's suppose that Tailscale instance is mymachine.primary.ts.net at 100.1.2.3.

We now want to setup another tailscaled instance on mymachine that is registered to the "secondary" tailnet under the domain secondary.ts.net. We can do so by starting up another tailscaled as follows:

STATE_DIR=/path/to/some/state-directory
LISTEN_ADDR=100.1.2.3:1080
tailscaled \
	--socket $STATE_DIR/tailscaled.sock \
	--state=$STATE_DIR/tailscaled.state \
	--statedir=$STATE_DIR \
	--tun=userspace-networking \
	--socks5-server=$LISTEN_ADDR

where:

  • STATE_DIR is some directory to hold persistent state files.
  • LISTEN_ADDR is the address to serve a SOCKS5 proxy on.

This starts up a new tailscaled instance running in userspace mode. It does not start up any kernal-level tunneling devices. It communicates to the Tailscale control server as tailscaled usually does, but all the networking logic occurs within the tailscaled process itself (i.e., it processes IP packets manually and implements a TCP stack). You will need to register this node with the "secondary" tailnet:

tailscale --socket=$STATE_DIR/tailscaled.sock up --authkey=$AUTH_KEY

In the example above, we have it listen to only the Tailscale network interface of the "primary" tailnet. As a security precaution, you should lock down the ACLs on the "primary" tailnet to only allow trusted devices to reach $LISTEN_ADDR. Anything that can reach the proxy node can make requests into the "secondary.ts.net" tailnet with the privileges of the proxy node. To reduce the scope of what can use the SOCKS5 proxy, you can also listen on localhost:1080 instead to only allow local connections.

The service setup might differ on other operating systems (e.g., whether to use systemd or some other service manager), but the flags passed to tailscaled should be identical as userspace networking does not depend on any operating system specific functionality. How to setup tailscaled as a persistent service on your given operating system is an exercise for the reader.

Proxy Setup

Use of a SOCKS5 proxy differs for each operating system, but almost every system supports use of a "proxy auto-configuration" (PAC) script to dynamically dictate how to proxy each HTTP request.

An example PAC script is as follows:

function FindProxyForURL(url, host) {
	if (shExpMatch(host, "*.secondary.ts.net")) {
		return "SOCKS5 100.1.2.3:1080";
	}
	return "DIRECT";
}

This script proxies any request where the hostname matches "*.secondary.ts.net", otherwise it uses a direct connection. For proxied requests, it sends them to "100.1.2.3:1080", which is the node inside the "secondary" tailnet that hosts a SOCKS5 proxy. You may modify the script to have it support proxying requests for more tailnets.

Many operating systems only support a "proxy auto-configuration" (PAC) script from a web URL. If you need a way to host the PAC file through a web server, you can use python3 to start a light-weight server:

python3 -m http.server 8080

This starts a file server serving from the current working directory on port 8080. It is an exercise to the reader to make this a persistent service if necessary.

For the steps below, we assume that we have a PAC script loadable at:

http://localhost:8080/proxy.pac

If you are hosting the PAC file on a machine that is only reachable via Tailscale, then beware that HTTP connections may not work while Tailscale is down.

Setup on Windows 10

  1. Open the system "Settings"
  2. Go to "Network & Internet"
  3. Go to "Proxy"
  4. Enable "Use setup script"
  5. Specify the URL to reach the PAC script for the "Script address"

Setup on ChromeOS

  1. Open the system "Settings"
  2. Go to "Network"
  3. Click on "VPN"
  4. Click on "Tailscale: Tailscale"
  5. Expand the "Proxy" section
  6. Select "Automatic proxy configuration" for "Connection type"
  7. Specify the URL to reach the PAC script for the "Autoconfiguration URL"

This setup in ChromeOS is unique in that the PAC is only used if Tailscale is running.

@drio

drio commented Oct 8, 2023

Copy link
Copy Markdown

Thank you dsnet.

For those of you on OSX (tested on Ventura 13.5.2), to enable socks5 go to system preferences and search for proxies. Once there, click in automatic proxy configuration and enter your URL.

If you need to update your proxy URL, you can use the following script which will restart the network interfaces that have socks5 enabled:

#!/bin/sh -u
networksetup -listallnetworkservices | awk 'NR>1' | while read SERVICE ; do
  if networksetup -getautoproxyurl "$SERVICE" | grep '^Enabled: Yes' >/dev/null; then
    networksetup -setautoproxystate "$SERVICE" off
    sleep 2
    networksetup -setautoproxystate "$SERVICE" on
    echo "$SERVICE" bounced.
  fi
done

@dsnet

dsnet commented Oct 12, 2023

Copy link
Copy Markdown
Author

Oh very cool, I just discovered that you can also do SSH through the proxy:

tailscale --socket=$STATE_DIR/tailscaled.sock ssh user@host

This solves probably the most two common usages of networking (HTTP and SSH)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment