Mount share only if connected to Tailscale

Setup: Pretty common ig, systemd, network manager, systemd-resolved. Pretty much always connected to Tailscale.

Problem: Mount an NFS/Samba share only if connected through tailscale. I am aware that 100.x.y.z is unroutable though local networks due to NAT and thus can directly be used in /etc/fstab but I would like no packet to exit the machine unnecessarily.

Do you have any solutions? Or am I barking up the wrong tree and there’s an alternative to expecting an auto-mount? Closest I’ve come is a systemd.path triggering a systemd.service that checks the status of the tailscale0 interface and mounts if it is up. Thoughts? Any guidance would be appreciated, thanks!

1 Like

It’s definitely a wishlist item for me as well to offer a definable list or even automatically mount shares on a Tailscale connection.

Maybe a quick script that runs a ping check against hello.ts.net; if it can reasonably reach it, utilize autofs to mount the share? Wrap that up in a systemd service that triggers whenever networking comes back online

2 Likes

Was thinking pretty much in the same vein but since I was spending time on it anyways, figured it should be extendable.

Quick rundown of my solution:

Set up a 10s timer to test

systemctl is-active --quiet tailscaled.service && [[ $(tailscale status --peers=false --json=true | jq -r '.Self.Online') = "true" ]]

and start / stop tailscale-online.target. Now, any service that hooks to the target will start and stop with tailscale.

So all that is left is to write out <mountpoint>.automount (with RequiredBy=tailscale-online.target) and <mountpoint>.mount to get autofs mounts on directory access! Just rebooted a couple times and tested manually, seems to do the trick, will update here if I find any corner cases as time passes.

Bonus Tip:

If testing with tailscale status --peers=true, we get the online status of every device and then one can feasibly create something like <hostname>-online.target which will be active only if a particular device is up and connected to the tailnet, great for people who host stuff from laptops / Pis etc.

2 Likes

This looks like exactly what I need but could you please spell out a little more exactly what files need to be created and the commands to link them?

1 Like

Sorry for the delayed response, hope it helps. Documenting anyway as it might help others as well.

The idea is to have a systemd target (tailscale-online.target) that we ensure is enabled if and only if tailscale is up and vice versa.

To do that write and enable tailscale-dispatcher.timer:

[Unit]
Description=Checks for tailscale state and syncs with `tailscale-online.target`

BindsTo=tailscaled.service
After=tailscaled.service

[Timer]
OnBootSec=0
OnUnitInactiveSec=10
AccuracySec=1

[Install]
WantedBy=tailscaled.service

The timer above will start tailscale-dispatcher.service (no need to start or enable):

[Unit]
Description=Checks for tailscale state and syncs with `tailscale-online.target`

Requisite=tailscaled.service
After=tailscaled.service

[Service]
Type=oneshot
ExecStart=tailscale-dispatcher

The service above will run the script tailscale-dispatcher (give executable perms and put in $PATH:

#! /usr/bin/env zsh

get-state() {
    if systemctl is-active --quiet tailscaled.service && [[ $(tailscale status --peers=false --json=true | jq -r '.Self.Online') = "true" ]]; then
        current_state="online"
    else
        current_state="offline"
    fi

}

transition() {
    if [[ "$current_state" != "$prev_state" ]]; then
        if [[ "$current_state" = "online" ]]; then
            systemctl start tailscale-online.target
        else
            systemctl stop tailscale-online.target
        fi
        echo "$current_state" > /tmp/tailscale-state
    fi
}

check-state() {
    get-state
    if [[ -s /tmp/tailscale-state ]]; then
        prev_state=$(</tmp/tailscale-state)
    else
        prev_state="offline"
        # uncomment if state has to be written even if still offline on all invocations before first transition to online
        # touch /tmp/tailscale-state
        # echo "$current_state" > /tmp/tailscale-state
    fi
    transition
}

main() {
    check-state
}

main "$@"

Finally write tailscale-online.target (no need to start or enable):

[Unit]
Description=Synced with tailscale status

BindsTo=tailscaled.service
After=tailscaled.service

Now if you have a service that works only in a tailnet (say nfs-mounter.service), append the following to its service file and enable it (no need to start):

[Install]
RequiredBy=tailscale-online.target

Now nfs-mounter.service will start and stop along with tailscale-online.target, as expected. Ymmv, but I’ve been using this in my work laptop for the past few months and it has been going well except when resuming out of hibernation with no Wi-Fi and open files, but connecting fixes that as well.

PS: I know it looks over-engineered and is literally equivalent to a cron bash script, but I wanted it to be generic enough to extend to other tail’ed services and also reuse the Dispatcher idiom followed by other network managers (NetworkManager and systemd-networkd for ex.).

PPS: I will allow any strongly-worded rebuke if you find any bash stupidities in the script. Not exactly my strong-suit.

I adapted your solution to work on NixOS, and wanted to share! I was having issues with coredns starting before tailscale was online when booting.

systemd.timers."tailscale-dispatcher" = {
    bindsTo = [ "tailscaled.service" ];
    after = [ "tailscaled.service" ];
    wantedBy = [ "tailscaled.service" ];
    timerConfig = {
      OnBootSec = "0";
      OnUnitInactiveSec = "10";
      AccuracySec = "1";
    };
  };
  systemd.services."tailscale-dispatcher" = {
    requisite = [ "tailscaled.service" ];
    after = [ "tailscaled.service" ];
    serviceConfig.Type = "oneshot";
    script = with pkgs; ''
      get-state() {
        if ${systemd}/bin/systemctl is-active --quiet tailscaled.service && [[ $(${tailscale}/bin/tailscale status --peers=false --json=true | ${jq}/bin/jq -r '.Self.Online') = "true" ]]; then
          current_state="online"
        else
          current_state="offline"
        fi
      }
      transition() {
        if [[ "$current_state" != "$prev_state" ]]; then
          if [[ "$current_state" = "online" ]]; then
            ${systemd}/bin/systemctl start tailscale-online.target
          else
            ${systemd}/bin/systemctl stop tailscale-online.target
          fi
          echo "$current_state" > /tmp/tailscale-state
        fi
      }
      check-state() {
        get-state
        if [[ -s /tmp/tailscale-state ]]; then
          prev_state=$(</tmp/tailscale-state)
        else
          prev_state="offline"
        fi
        transition
      }
      check-state
    '';
  };
  systemd.targets."tailscale-online" = {
    after = [ "tailscaled.service" ];
    bindsTo = [ "tailscaled.service" ];
  };

  systemd.services.coredns.requisite = [ "tailscale-online.target" ];
  systemd.services.coredns.after = [ "tailscale-online.target" ];
  systemd.services.coredns.requiredBy = [ "tailscale-online.target" ];
  ...

coredns will fail to start once, then will get started when tailscale goes online. There’s probably a way to fix that, but this is good enough for me.