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?

2 Likes

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.

1 Like

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.

This looks like a really good answer to this question, but if your question is more like “how can I make sure that mount -a doesn’t fail because it happens before tailscale cranks up”, then (thanks to Ubuntu's boot order for Tailscale service - #7 by raggi) all you need is

systemctl add-wants nfs-mountd.service tailscaled

This was the first thing I tried, but there is a delay between tailscaled.service transitioning to active and any other tail’ed host being routable (tailscale ping). So services ordered After= it still fail if they start to listen before it is routable. Additionally if the device itself loses internet, any app with open filesystem handles will hang.

May be interesting to explore converting tailscaled.service to a Type=notify instead of Type=exec so the state transitions to active only after tailscale ping returns 0. But I doubt even that would fix file-managers hanging between disconnects.

PS: I just checked tailscaled.service and looks like tailscaled now has a --state argument, whatever that does. Just documenting here for anyone to dig in. For now I’m content with the above jerry-rig, been working for months as it’s persistent and explicitly umount’s some ~5 secs after losing internet.

1 Like