Add new post about NFS on Tailscale
This commit is contained in:
@@ -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.
|
||||||
Reference in New Issue
Block a user