at master 9.0 kB view raw
1{ 2 config, 3 lib, 4 pkgs, 5 ... 6}: 7 8with lib; 9 10let 11 cfg = config.services.tailscale; 12 isNetworkd = config.networking.useNetworkd; 13in 14{ 15 meta.maintainers = with maintainers; [ 16 mbaillie 17 mfrw 18 ]; 19 20 options.services.tailscale = { 21 enable = mkEnableOption "Tailscale client daemon"; 22 23 port = mkOption { 24 type = types.port; 25 default = 41641; 26 description = "The port to listen on for tunnel traffic (0=autoselect)."; 27 }; 28 29 interfaceName = mkOption { 30 type = types.str; 31 default = "tailscale0"; 32 description = ''The interface name for tunnel traffic. Use "userspace-networking" (beta) to not use TUN.''; 33 }; 34 35 permitCertUid = mkOption { 36 type = types.nullOr types.nonEmptyStr; 37 default = null; 38 description = "Username or user ID of the user allowed to to fetch Tailscale TLS certificates for the node."; 39 }; 40 41 disableTaildrop = mkOption { 42 default = false; 43 type = types.bool; 44 description = "Whether to disable the Taildrop feature for sending files between nodes."; 45 }; 46 47 disableUpstreamLogging = mkOption { 48 default = false; 49 type = types.bool; 50 description = "Whether to disable Tailscaled from sending debug logging upstream."; 51 }; 52 53 package = lib.mkPackageOption pkgs "tailscale" { }; 54 55 openFirewall = mkOption { 56 default = false; 57 type = types.bool; 58 description = "Whether to open the firewall for the specified port."; 59 }; 60 61 useRoutingFeatures = mkOption { 62 type = types.enum [ 63 "none" 64 "client" 65 "server" 66 "both" 67 ]; 68 default = "none"; 69 example = "server"; 70 description = '' 71 Enables settings required for Tailscale's routing features like subnet routers and exit nodes. 72 73 To use these these features, you will still need to call `sudo tailscale up` with the relevant flags like `--advertise-exit-node` and `--exit-node`. 74 75 When set to `client` or `both`, reverse path filtering will be set to loose instead of strict. 76 When set to `server` or `both`, IP forwarding will be enabled. 77 ''; 78 }; 79 80 authKeyFile = mkOption { 81 type = types.nullOr types.path; 82 default = null; 83 example = "/run/secrets/tailscale_key"; 84 description = '' 85 A file containing the auth key. 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`. 89 ''; 90 }; 91 92 authKeyParameters = mkOption { 93 type = types.submodule { 94 options = { 95 ephemeral = mkOption { 96 type = types.nullOr types.bool; 97 default = null; 98 description = "Whether to register as an ephemeral node."; 99 }; 100 preauthorized = mkOption { 101 type = types.nullOr types.bool; 102 default = null; 103 description = "Whether to skip manual device approval."; 104 }; 105 baseURL = mkOption { 106 type = types.nullOr types.str; 107 default = null; 108 description = "Base URL for the Tailscale API."; 109 }; 110 }; 111 }; 112 default = { }; 113 description = '' 114 Extra parameters to pass after the auth key. 115 See <https://tailscale.com/kb/1215/oauth-clients#registering-new-nodes-using-oauth-credentials> 116 ''; 117 }; 118 119 extraUpFlags = mkOption { 120 description = '' 121 Extra flags to pass to {command}`tailscale up`. Only applied if {option}`services.tailscale.authKeyFile` is specified. 122 ''; 123 type = types.listOf types.str; 124 default = [ ]; 125 example = [ "--ssh" ]; 126 }; 127 128 extraSetFlags = mkOption { 129 description = "Extra flags to pass to {command}`tailscale set`."; 130 type = types.listOf types.str; 131 default = [ ]; 132 example = [ "--advertise-exit-node" ]; 133 }; 134 135 extraDaemonFlags = mkOption { 136 description = "Extra flags to pass to {command}`tailscaled`."; 137 type = types.listOf types.str; 138 default = [ ]; 139 example = [ "--no-logs-no-support" ]; 140 }; 141 }; 142 143 config = mkIf cfg.enable { 144 environment.systemPackages = [ cfg.package ]; # for the CLI 145 systemd.packages = [ cfg.package ]; 146 systemd.services.tailscaled = { 147 after = lib.mkIf (config.networking.networkmanager.enable) [ "NetworkManager-wait-online.service" ]; 148 wantedBy = [ "multi-user.target" ]; 149 path = [ 150 (builtins.dirOf config.security.wrapperDir) # for `su` to use taildrive with correct access rights 151 pkgs.procps # for collecting running services (opt-in feature) 152 pkgs.getent # for `getent` to look up user shells 153 pkgs.kmod # required to pass tailscale's v6nat check 154 ] 155 ++ lib.optional config.networking.resolvconf.enable config.networking.resolvconf.package; 156 serviceConfig.Environment = [ 157 "PORT=${toString cfg.port}" 158 ''"FLAGS=--tun ${lib.escapeShellArg cfg.interfaceName} ${lib.concatStringsSep " " cfg.extraDaemonFlags}"'' 159 ] 160 ++ (lib.optionals (cfg.permitCertUid != null) [ 161 "TS_PERMIT_CERT_UID=${cfg.permitCertUid}" 162 ]) 163 ++ (lib.optionals (cfg.disableTaildrop) [ 164 "TS_DISABLE_TAILDROP=true" 165 ]) 166 ++ (lib.optionals (cfg.disableUpstreamLogging) [ 167 "TS_NO_LOGS_NO_SUPPORT=true" 168 ]); 169 # Restart tailscaled with a single `systemctl restart` at the 170 # end of activation, rather than a `stop` followed by a later 171 # `start`. Activation over Tailscale can hang for tens of 172 # seconds in the stop+start setup, if the activation script has 173 # a significant delay between the stop and start phases 174 # (e.g. script blocked on another unit with a slow shutdown). 175 # 176 # Tailscale is aware of the correctness tradeoff involved, and 177 # already makes its upstream systemd unit robust against unit 178 # version mismatches on restart for compatibility with other 179 # linux distros. 180 stopIfChanged = false; 181 }; 182 183 systemd.services.tailscaled-autoconnect = mkIf (cfg.authKeyFile != null) { 184 after = [ "tailscaled.service" ]; 185 wants = [ "tailscaled.service" ]; 186 wantedBy = [ "multi-user.target" ]; 187 serviceConfig = { 188 Type = "notify"; 189 }; 190 path = [ 191 cfg.package 192 pkgs.jq 193 ]; 194 enableStrictShellChecks = true; 195 script = 196 let 197 paramToString = v: if (builtins.isBool v) then (lib.boolToString v) else (toString v); 198 params = lib.pipe cfg.authKeyParameters [ 199 (lib.filterAttrs (_: v: v != null)) 200 (lib.mapAttrsToList (k: v: "${k}=${paramToString v}")) 201 (builtins.concatStringsSep "&") 202 (params: if params != "" then "?${params}" else "") 203 ]; 204 in 205 # bash 206 '' 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 233 done 234 ''; 235 }; 236 237 systemd.services.tailscaled-set = mkIf (cfg.extraSetFlags != [ ]) { 238 after = [ "tailscaled.service" ]; 239 wants = [ "tailscaled.service" ]; 240 wantedBy = [ "multi-user.target" ]; 241 serviceConfig = { 242 Type = "oneshot"; 243 }; 244 script = '' 245 ${lib.getExe cfg.package} set ${escapeShellArgs cfg.extraSetFlags} 246 ''; 247 }; 248 249 boot.kernel.sysctl = mkIf (cfg.useRoutingFeatures == "server" || cfg.useRoutingFeatures == "both") { 250 "net.ipv4.conf.all.forwarding" = mkOverride 97 true; 251 "net.ipv6.conf.all.forwarding" = mkOverride 97 true; 252 }; 253 254 networking.firewall.allowedUDPPorts = mkIf cfg.openFirewall [ cfg.port ]; 255 256 networking.firewall.checkReversePath = mkIf ( 257 cfg.useRoutingFeatures == "client" || cfg.useRoutingFeatures == "both" 258 ) "loose"; 259 260 networking.dhcpcd.denyInterfaces = [ cfg.interfaceName ]; 261 262 systemd.network.networks."50-tailscale" = mkIf isNetworkd { 263 matchConfig = { 264 Name = cfg.interfaceName; 265 }; 266 linkConfig = { 267 Unmanaged = true; 268 ActivationPolicy = "manual"; 269 }; 270 }; 271 }; 272}