lol

nixos/tailscale: tailscaled-autoconnect use Type=notify, wait for Running

Previously the `Starting` state was missed, allowing the service to
complete before the interface was ready, causing services that bind to
Tailscale IPs to fail to start.

Now waits for Tailscale to report `Running` and notifies systemd
accordingly.

Switch the unit to Type=notify to timeout if there is no connection.

Remove `NeedsMachineAuth` gating since it requires client approval in
the console.

+36 -10
+36 -10
nixos/modules/services/networking/tailscale.nix
··· 84 84 description = '' 85 85 A file containing the auth key. 86 86 Tailscale will be automatically started if provided. 87 + 88 + Services that bind to Tailscale IPs should order using {option}`systemd.services.<name>.after` `tailscaled-autoconnect.service`. 87 89 ''; 88 90 }; 89 91 ··· 116 118 117 119 extraUpFlags = mkOption { 118 120 description = '' 119 - Extra flags to pass to {command}`tailscale up`. Only applied if `authKeyFile` is specified."; 121 + Extra flags to pass to {command}`tailscale up`. Only applied if {option}`services.tailscale.authKeyFile` is specified. 120 122 ''; 121 123 type = types.listOf types.str; 122 124 default = [ ]; ··· 183 185 wants = [ "tailscaled.service" ]; 184 186 wantedBy = [ "multi-user.target" ]; 185 187 serviceConfig = { 186 - Type = "oneshot"; 188 + Type = "notify"; 187 189 }; 188 - # https://github.com/tailscale/tailscale/blob/v1.72.1/ipn/backend.go#L24-L32 190 + path = [ 191 + cfg.package 192 + pkgs.jq 193 + ]; 194 + enableStrictShellChecks = true; 189 195 script = 190 196 let 191 - statusCommand = "${lib.getExe cfg.package} status --json --peers=false | ${lib.getExe pkgs.jq} -r '.BackendState'"; 192 197 paramToString = v: if (builtins.isBool v) then (lib.boolToString v) else (toString v); 193 198 params = lib.pipe cfg.authKeyParameters [ 194 199 (lib.filterAttrs (_: v: v != null)) ··· 197 202 (params: if params != "" then "?${params}" else "") 198 203 ]; 199 204 in 205 + # bash 200 206 '' 201 - while [[ "$(${statusCommand})" == "NoState" ]]; do 202 - sleep 0.5 207 + getState() { 208 + tailscale status --json --peers=false | jq -r '.BackendState' 209 + } 210 + 211 + lastState="" 212 + while state="$(getState)"; do 213 + if [[ "$state" != "$lastState" ]]; then 214 + # https://github.com/tailscale/tailscale/blob/v1.72.1/ipn/backend.go#L24-L32 215 + case "$state" in 216 + NeedsLogin) 217 + echo "Server needs authentication, sending auth key" 218 + tailscale up --auth-key "$(cat ${cfg.authKeyFile})${params}" ${escapeShellArgs cfg.extraUpFlags} 219 + ;; 220 + Running) 221 + echo "Tailscale is running" 222 + systemd-notify --ready 223 + exit 0 224 + ;; 225 + *) 226 + echo "Waiting for Tailscale State = Running or systemd timeout" 227 + ;; 228 + esac 229 + fi 230 + echo "State = $state" 231 + lastState="$state" 232 + sleep .5 203 233 done 204 - status=$(${statusCommand}) 205 - if [[ "$status" == "NeedsLogin" || "$status" == "NeedsMachineAuth" ]]; then 206 - ${lib.getExe cfg.package} up --auth-key "$(cat ${cfg.authKeyFile})${params}" ${escapeShellArgs cfg.extraUpFlags} 207 - fi 208 234 ''; 209 235 }; 210 236