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