Skip to content

Instantly share code, notes, and snippets.

@drio
Last active February 4, 2025 09:16
Show Gist options
  • Save drio/b90b772a2857e873bf95214841ee95d1 to your computer and use it in GitHub Desktop.
Save drio/b90b772a2857e873bf95214841ee95d1 to your computer and use it in GitHub Desktop.
Tailscale: accessing resources from different tailnets without switching between them

@dsnet has a much improved version of this concept.

Tailscale: Accessing resources between tailnets without account switching

@dsnet suggested a method he uses to access web resources from his work tailnet while connected to his personal tailnet. Those resources are web resources (like dashboards). You may also want ssh access, I will talk about that later. @dsnet uses a chrome book and ssh into a home machine to do his work. Of course, he uses tailscale.

Notice we are going to use an OSX machine but this should be very similar in other OSes. The part that will change the most will probably be the PAC setup.

Here are the steps (will talk in more depth about them later):

  1. switch to your personal tailnet (PTN)
  2. ssh into a box that is connected to your work tailnet (WTN)
  3. run another instance of tailscaled manually:

$ tailscaled --socket=/tmp/ts.sock --tun=userspace-networking --socks5-server=localhost:1055 --outbound-http-proxy-listen=localhost:1055

  1. join your personal tailnet using a pre-auth key:

tailscale --socket=/tmp/ts.sock up --authkey=tskey-auth-xxxxx

At this point the machine is in both tailnets. πŸ”₯

  1. In your OSX box, go to system preferences and search for proxies (under network).
    Select automatic proxy configuration and add a URL that points to your PAC file. I am not sure why but this has to
    be an URL. Your pac file should look like:
function FindProxyForURL(url, host) {
    if (shExpMatch(host, "*grafana.xxxx.ts.net"))
      return "PROXY the-machine:1055";

    // by default use no proxy
    return "DIRECT";
}

We are telling the OS, if anh process wants to access a URL with domain grafana.xxxx.ts.net use this sock5 proxy: the-machine:1055. the-machine is the hostname of the machine in our personal tailnet.

Notice that you will have to take your interface up and down for a new pac file to reload. You can use this script:

#!/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

At this point, you can access your grafana dashboards under *grafana.xxxx.ts.net from your browser without switching tailnets.

There are a lot of unknowns (the magic of tailscale?) I know. I am going to expand this in the future to go a bit more in detail. In particular, how does --tun=userspace-networking really work? I know tailscaled is built with gvisor which implements (to my understanding) tons of OS/kernel functionality at user level. But how does really work? At some point you have to send a packet to the network interface (NIC). How can you do that from userland? Those questions will be answer once I figure it out.

Enjoy Tailscale.

@drio
Copy link
Author

drio commented Feb 3, 2025

I've had machines running in multiple tailnets for a while, and I have several insights to share. I've also deepened my understanding of what happens when you run the userland TCP/IP stack in Tailscale.

First, adding a machine to multiple tailnets (with limitations) is pretty straightforward. Start by following the regular method to make your machine join tailnet A (your main tailnet). Then, use the following commands to make the machine join tailnet B:

sk="./ts.socket"
key="gen-keh-in-ts-ui"
tailscaled --socket=$sk --tun=userspace-networking &> tailscaled.log &
tailscale --socket=$sk up --authkey=$key --reset

You can add this as a systemd service, but I just run it in a tmux session that I ensure restarts on reboots via cron. Read more about this approach here.

What's actually happening here? First, tailscaled is the process that handles all packet movement and is responsible for creating the WireGuard connections. Without userspace-networking, it uses the OS tun functionality provided by the kernel to handle Tailscale traffic. This means you end up with a virtual networking interface visible through standard networking CLI tools. This interface will have one of the 100.x.x.x IP addresses as it belongs to the tailnet. Any traffic that goes to the ip address associated to this interface will be delivered by the kernel to the process (via a file descriptor) that created the tun device.

When we use the userspace-networking flag, tailscaled switches to using gVisor's network stack to implement all networking functionality. You'll still have another 100.x.x.x address in the other tailnet, but it won't be visible in the host OS. All traffic related to the second tailnet flows through that userspace TCP/IP stack. tailscaled receives packets from other machines in the tailnet and communicates with them by creating WireGuard tunnels - essentially encrypted packets sent over UDP.

There is one caveat with this method: processes in the host machine won't be able to reach machines in tailnet B. That's fine for my use case, as I primarily want access to that machine from a few machines. You can send TCP/UDP traffic to any port on that machine, assuming you have the proper ACLs, host firewall configuration, and the service you're trying to reach is listening on all interfaces.

So there you have it. Please comment here if you have any feedback or want to help improve my mental model of how userspace networking works in Tailscale.​​​​​​​​​​​​​​​​

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