NFS Over Tailscale - or any VPN
The Problem
For my home media consumption I use a media server that runs in a small VM with passthru of a GPU for transcoding needs. The files are hosted on my local NAS box, which handles running services like Pinchflat and also holds the digitized copies from my wife’s CD collection and my home photo and family video collection. The NAS exports each of these 3 collections as an NFS share that everyone on the LAN can mount, including the media server VM.
The media server and VM both are connected to my Tailnet in order to facilitate easy interoperation with my phone, my Kubernetes cluster, and other pieces of my personal infrastructure. Although the VM and NAS are on the same actual physical LAN and could directly connect over IP address, for the sake of continuity I keep all of my infrastructure communicating over the VPN.
This led to a problem: the VPN sits in the startup chain later than the mounts
are being attached. While the system is smart enough to reserve NFS automounts
for after reaching the networking-online.target, it was unaware that my
particular needs include waiting for Tailscale to also be started.
The Solution
So this is where I had to step outside of /etc/fstab and instead create a
systemd mount file instead. /etc/fstab allows you to specify as an option
a dependency on another mount using the option x-systemd.requires=foo, but
I was unable to convince that option to allow me to specify a foo.service.
It only seems to want to accept foo and assumes that its type is a .mount
file. But, by crafting an actual systemd unit file for the mount directly, I
could control this behavior.
In NixOS I configured it as follows:
systemd.mounts = let
nfs = name: {
what = "nas.my-domain.ts.net:/mnt/path/to/share/${name}";
type = "nfs";
name = "${name}.mount";
where = "/${name}";
requires = [ "tailscaled-autoconnect.service" ];
after = [ "tailscaled-autoconnect.service" ];
wantedBy = [ "multi-user.target" ];
mountConfig.Options = "_netdev,noexec,ro,timeo=50,retrans=5,soft";
};
in [
(nfs "photos")
(nfs "video")
(nfs "music")
];
The options here are pretty well mapped directly to the systemd unit file options that you might expect.
The Details
However, just for completeness’s sake I’ll show you one of the full systemd unit files it generated in case you are not on a NixOS system.
[Unit]
After=tailscaled-autoconnect.service
Requires=tailscaled-autoconnect.service
[Mount]
Options=_netdev,noexec,ro,timeo=50,retrans=5,soft
Type=nfs
What=nas1.private-redacted.ts.net:/mnt/all/video
Where=/video
[Install]
WantedBy=multi-user.target
Of course, in order for this to work for you, you will need a tailscaled-autoconnect.service
to wait on. It is not sufficient just to wait for the normal tailscaled.service unit
file that is normally present. That only starts the daemon, but the daemon can sometimes
take a bit to connect. That’s where NixOS module maintainers come to the rescue by
giving me this very helpful tailscaled-autoconnect service that I can latch on to. It has a
Type=notify configuration and just runs a shell script, which I will replicate here:
set -o errexit
set -o nounset
set -o pipefail
getState() {
tailscale status --json --peers=false | jq -r '.BackendState'
}
lastState=""
while state="$(getState)"; do
if [[ "$state" != "$lastState" ]]; then
# https://github.com/tailscale/tailscale/blob/v1.72.1/ipn/backend.go#L24-L32
case "$state" in
NeedsLogin)
echo "Server needs authentication, sending auth key"
tailscale up --auth-key "$(cat /run/agenix/tailscale-key)?preauthorized=true" --hostname jellyfin
;;
Running)
echo "Tailscale is running"
systemd-notify --ready
exit 0
;;
*)
echo "Waiting for Tailscale State = Running or systemd timeout"
;;
esac
fi
echo "State = $state"
lastState="$state"
sleep .5
done
Quickly looking through this script it is very straightforward. It loops every 0.5 seconds, calling
the tailscale up command when the system needs to be logged in and passing it my Tailscale OAuth key.
Once the system is up and Running, meaning that the connection to Tailscale is completed, it will
notify systemd that the service has come up and then exit gracefully.
Obviously, tweak these scripts and needs to your own use. In NixOS, most of them are already present
and all you need is to start your mount after tailscaled-autoconnect.service. A similar technique
could easily be adapted to other flavors of Linux and to other VPN providers.