Files
jpporta 726681e5c2 Squashed commit of the following:
commit 19ed08c375be6d6a0b344ab1b9d3f76f3c29514b
Author: João Porta <jpedro.porta@gmail.com>
Date:   Fri Jul 17 23:12:12 2026 +0000

    zsh

commit 19df08188fa07b40b9b28a4665b015c4fa525dff
Merge: 577d753 4e4cc38
Author: João Porta <jpedro.porta@gmail.com>
Date:   Thu Jul 16 22:29:29 2026 +0000

    Merge remote-tracking branch 'refs/remotes/origin/tailscale' into tailscale

commit 577d753475be867146ebcacc79330a1e227043a5
Author: João Porta <jpedro.porta@gmail.com>
Date:   Thu Jul 16 15:29:53 2026 +0000

    tailscale

commit 4e4cc381b9e333592184490ea9f749fe87ebbbb9
Author: João Porta <jpedro.porta@gmail.com>
Date:   Thu Jul 16 15:29:53 2026 +0000

    tailscale
2026-07-17 23:15:07 +00:00

143 lines
5.1 KiB
Nix

{
config,
lib,
pkgs,
...
}:
let
cfg = config.custom.tailscale-daemon;
# The writter-deck is a non-NixOS host that can't create a real tailscale0
# interface (missing TUN drivers). Userspace networking avoids that need.
#
# Layout:
# - Socket: %t/tailscaled/tailscaled.sock (per-session tmpfs)
# - State: $HOME/.local/state/tailscale/... (persistent)
#
# In systemd specifiers:
# %t → $XDG_RUNTIME_DIR (=/run/user/$UID) — works in systemd 249+
# %S → $XDG_CONFIG_HOME (! NOT $XDG_STATE_HOME) — works in systemd 249+
#
# Because %S points at the wrong place, we hard-code the persistent state
# path under $HOME/.local/state/, which is the XDG_STATE_HOME default.
homeDir = config.home.homeDirectory;
stateDir = "${homeDir}/.local/state/tailscale";
stateFile = "${stateDir}/tailscaled.state";
# Becomes "$XDG_RUNTIME_DIR/tailscaled/tailscaled.sock" at runtime.
socketFile = "%t/tailscaled/tailscaled.sock";
in
{
options.custom.tailscale-daemon = {
enable = lib.mkEnableOption ''
Tailscale daemon (tailscaled) running as a systemd user service with
userspace networking. For non-NixOS hosts without TUN device drivers.
'';
acceptRoutes = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
If true, run `tailscale up --accept-routes` automatically on first
start. Otherwise you'll need to authenticate manually once.
'';
};
acceptDns = lib.mkOption {
type = lib.types.bool;
default = true;
description = "Whether to accept DNS settings from the coordination server.";
};
};
config = lib.mkIf cfg.enable {
# Create the persistent state dir up front so the systemd service can
# write to it immediately.
home.activation.createTailscaleDirs = lib.hm.dag.entryAfter [ "writeBoundary" ] ''
run mkdir -p ${lib.escapeShellArg stateDir}
run chmod 0700 ${lib.escapeShellArg stateDir}
'';
# Export the socket path (systemd-249-expanded) so any tool (systray,
# scripts) picks the user-scope daemon automatically. Note: this
# contains a literal `%t` which systemd will expand when the daemon
# or its consumers run.
home.sessionVariables.TAILSCALED_SOCKET = "/run/user/$(id -u)/tailscaled/tailscaled.sock";
home.packages = [
pkgs.tailscale
# Wrapper so the CLI client can find the user-scope socket without flags.
(pkgs.writeShellApplication {
name = "ts";
runtimeInputs = [ pkgs.tailscale ];
text = ''
exec tailscale --socket="/run/user/$(id -u)/tailscaled/tailscaled.sock" "$@"
'';
})
];
systemd.user.services.tailscaled = {
Unit = {
Description = "Tailscale daemon (userspace networking, no TUN device)";
# No "After=default.target" — it would deadlock since default.target
# is what brings us up. Just wait for the runtime dir to exist.
RequiresMountsFor = [ "%t" ];
# Survive transient network blips / flap.
StartLimitIntervalSec = 60;
StartLimitBurst = 5;
};
Service = {
Type = "notify";
ExecStart = lib.escapeShellArgs [
"${pkgs.tailscale}/bin/tailscaled"
"--tun=userspace-networking"
"--state=${stateFile}"
"--socket=${socketFile}"
];
# systemd-managed runtime dir:
# $XDG_RUNTIME_DIR/tailscaled (RuntimeDirectory, mode 0700)
# State is in $HOME/.local/state/tailscale and managed by us, not
# via StateDirectory, so we don't need to fight protectHome.
RuntimeDirectory = "tailscaled";
RuntimeDirectoryMode = "0700";
Restart = "on-failure";
RestartSec = 5;
# Light sandboxing. systemd user services can't drop capabilities
# the way the system unit can, so we keep restrictions to a
# conservative subset that doesn't fight journald.
PrivateTmp = true;
ProtectSystem = "strict";
ProtectHome = true;
ReadWritePaths = [ "%t/tailscaled" "${stateDir}" ];
};
Install.WantedBy = [ "default.target" "graphical-session.target" ];
};
# One-shot: authenticate on first start if not already logged in.
# Idempotent: tailscale up is a no-op once you're logged in.
systemd.user.services.tailscale-up = {
Unit = {
Description = "Tailscale: authenticate and connect";
After = [ "tailscaled.service" ];
Requires = [ "tailscaled.service" ];
};
Service = {
Type = "oneshot";
RemainAfterExit = true;
ExecStart = lib.escapeShellArgs ([
"${pkgs.tailscale}/bin/tailscale"
"--socket=${socketFile}"
"up"
(if cfg.acceptRoutes then "--accept-routes" else "--no-accept-routes")
(if cfg.acceptDns then "--accept-dns=true" else "--accept-dns=false")
]);
# Don't fail the whole stack if auth isn't completed yet.
SuccessExitStatus = [ 0 1 ];
};
Install.WantedBy = [ "default.target" "graphical-session.target" ];
};
};
}