From 0671f6f8878dd33979356b84013dde4778fba90d Mon Sep 17 00:00:00 2001 From: Greg Hellings Date: Sun, 26 Oct 2025 22:51:53 -0500 Subject: [PATCH] Add new post about NFS on Tailscale --- content/posts/nfs-over-tailscale.md | 130 ++++++++++++++++++++++++++++ 1 file changed, 130 insertions(+) create mode 100644 content/posts/nfs-over-tailscale.md diff --git a/content/posts/nfs-over-tailscale.md b/content/posts/nfs-over-tailscale.md new file mode 100644 index 0000000..1857b2b --- /dev/null +++ b/content/posts/nfs-over-tailscale.md @@ -0,0 +1,130 @@ +--- +title: "NFS Over Tailscale - or any VPN" +date: 2025-10-26T22:20:32-05:00 +draft: false +--- + +## The Problem +For my home media consumption I use a [media server](https://jellyfin.org/) +that runs in a small VM with passthru of a GPU for transcoding needs. The files +are hosted on [my local NAS](https://www.truenas.com/) box, which handles running +services like [Pinchflat](https://github.com/kieraneglin/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](https://tailscale.com/) +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](https://nixos.org) I configured it as follows: +```nix +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: + +```bash +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.