Tailscale on OpenBSD

I spent some time setting this up today evening and thought I’d post the steps here. Nothing fancy, just putting together various pieces actually.

I assume you know what Tailscale is; if not check out their website. Basically it is a mesh network built on top of Wireguard. Using it you can have all your devices both within your LAN(s) and outside be on one overlay network as if they are all on the same LAN and can talk to each other. It’s my new favourite thing!

Tailscale is open source and you can find the source on GitHub. Building it from source is easy too – just clone the repo (or download a release tarball and unzip) then build it so:

I tried that with the current -stable branch of OpenBSD, which is 6.7. Unfortunately that has Go 1.13 and compiling Tailscale requires Go 1.14 or above. So the first thing I did was switch to the -current branch. This is easy nowadays, all you have to do is sysupgrade -sand it will download a snapshot of -current, install, and reboot. Of course there is a risk to running -current but this is in my home lab so I am not too fussed.

With that out of the way go back to the downloaded source code and run the command above. This will install two binaries in $HOME/go/bin. Copy these over to /usr/local/sbin. (An earlier version of this post suggested copying them to /usr/sbin. Since then a reader Raf Czlonka wrote in to say /usr/sbin is reserved for the OS and /usr/local/{,s}bin is what must be used instead).

Next, create a startup script so tailscaled is automatically run by the rc scripts. Create a file called /etc/rc.d/tailscaled and put the following in it (I’d do doas nano -w /etc/rc.d/tailscaled and paste the below):

(Note: The above code has changed since I posted it initially).

Make the file you create non-writable & executable

Enable the service via rcctl:

And now you can start it via doas rcctl start tailscaled and also do a tailscale up to register your device as usual. If you reboot, tailscaled automatically connects to your Tailscale network.

More Info

Initially I had put in some more info above but decided to move it to a separate section here.

The rc script has three functions: rc_start(), rc_stop() and rc_check().

The rc_start() function will run tailscaled and send its logs to /var/log/messages (thanks to this article for the idea). I added rc_bg=YES to force starting the daemon in the background coz I noticed the system startup would pause a few seconds when starting up tailscaled. I could have added an & after logger -t tailscaled manually, but using rc_bg=YES is the recommended way.

The default rc_start() function is the following: "${daemon} ${daemon_flags} ${_bg}" So what I am doing is modify that to also add some logging.

The rc_stop() function calls tailscaled with the --cleanup switch to clean things up. It is supposed to stop the process too but doesn’t (some OpenBSD quirk I guess) so I kill it manually. I output this to the logs.

The default rc_start() function is the following: pkill -xf "${pexp}". It simply kills the process. That’s what I too eventually do but I try and do the cleanup anyways. The default ${pexp} variable has the daemon command line and all the flags; I set ${pexp} to be just the tailscaled name via pexp="tailscaled".

One of the errors thrown by --cleanup was about not finding the interface:

This is probably due to wireguard-go and OpenBSD (Tailscale uses wireguard-go). It looks like wireguard-go needs you to specify the tunnel interface manually in OpenBSD:

Since the tun driver cannot have arbitrary interface names, you must either use tun[0-9]+ for an explicit interface name or tun to have the program select one for you. If you choose tun as the interface name, and the environment variable WG_TUN_NAME_FILE is defined, then the actual name of the interface chosen by the kernel is written to the file specified by that variable.

I didn’t have any issues starting tailscaled without a tunnel specified, but considering it gave me an error when trying to stop I decided to specify the tunnel both when starting up and shutting down. Of course I’d also want to have the option to override this so I take advantage of the daemon_flags option of OpenBSD’s rc system for this.

Typically you’d configure a daemon via /etc/rc.conf.local using the daemon_flags variable (replace “daemon” with “tailscaled” in this case). If daemon_flags=NO then the daemon isn’t started while if daemon_flags= (empty) then it is started. You can also override any flags by specifying something to daemon_flags. With tailscaled I am passing the --port 0 --tun tun0 as flags by default to tell it to automatically select a port to listen to (--port 0) and use tun0 as the tunnel interface (--tun 0) but if someone wants to override that all they need to do is use a different set of parameters in their tailscaled_flags.

I also wanted to explicitly specify the state and socket files to tailscaled. This is not strictly needed as they default (on OpenBSD) to whatever I have specified in the rc script, but I thought I’d make it explicit anyways. That’s why I add these to the daemon variable.

Lastly, I have an rc_check() function so rcctl check tailscaled works without always showing failed. By default rc_check() does an pgrep -xf ${pexp} and that wasn’t working for me because -f checks for the process arguments too. So I override it to simply do pgrep -x ${pexp} – i.e. not bother with the arguments.


Update: Someone kindly logged a bug report for --cleanup not working.

Further Update: I modified the blog post with a revised script and also added more explanations to the rc script as I tweaked it a bit. The script has been revised a few times with the last change made on 29th Oct 2020.

Update (12th Dec 2022): Now and then I keep getting questions on TailScale and OpenBSD. Today I realized that’s coz this blog post seems to be the top result when searching for these words. Tailscale now has support for OpenBSD and FreeBSD. It doesn’t appear in their Download page, but if you look at the script they provide under the Linux section it caters to these two BSDs. That’s probably a better bet than this blog post. :)