at 25.11-pre 8.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 package = lib.mkPackageOption pkgs "tailscale" { }; 48 49 openFirewall = mkOption { 50 default = false; 51 type = types.bool; 52 description = "Whether to open the firewall for the specified port."; 53 }; 54 55 useRoutingFeatures = mkOption { 56 type = types.enum [ 57 "none" 58 "client" 59 "server" 60 "both" 61 ]; 62 default = "none"; 63 example = "server"; 64 description = '' 65 Enables settings required for Tailscale's routing features like subnet routers and exit nodes. 66 67 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`. 68 69 When set to `client` or `both`, reverse path filtering will be set to loose instead of strict. 70 When set to `server` or `both`, IP forwarding will be enabled. 71 ''; 72 }; 73 74 authKeyFile = mkOption { 75 type = types.nullOr types.path; 76 default = null; 77 example = "/run/secrets/tailscale_key"; 78 description = '' 79 A file containing the auth key. 80 Tailscale will be automatically started if provided. 81 ''; 82 }; 83 84 authKeyParameters = mkOption { 85 type = types.submodule { 86 options = { 87 ephemeral = mkOption { 88 type = types.nullOr types.bool; 89 default = null; 90 description = "Whether to register as an ephemeral node."; 91 }; 92 preauthorized = mkOption { 93 type = types.nullOr types.bool; 94 default = null; 95 description = "Whether to skip manual device approval."; 96 }; 97 baseURL = mkOption { 98 type = types.nullOr types.str; 99 default = null; 100 description = "Base URL for the Tailscale API."; 101 }; 102 }; 103 }; 104 default = { }; 105 description = '' 106 Extra parameters to pass after the auth key. 107 See https://tailscale.com/kb/1215/oauth-clients#registering-new-nodes-using-oauth-credentials 108 ''; 109 }; 110 111 extraUpFlags = mkOption { 112 description = '' 113 Extra flags to pass to {command}`tailscale up`. Only applied if `authKeyFile` is specified."; 114 ''; 115 type = types.listOf types.str; 116 default = [ ]; 117 example = [ "--ssh" ]; 118 }; 119 120 extraSetFlags = mkOption { 121 description = "Extra flags to pass to {command}`tailscale set`."; 122 type = types.listOf types.str; 123 default = [ ]; 124 example = [ "--advertise-exit-node" ]; 125 }; 126 127 extraDaemonFlags = mkOption { 128 description = "Extra flags to pass to {command}`tailscaled`."; 129 type = types.listOf types.str; 130 default = [ ]; 131 example = [ "--no-logs-no-support" ]; 132 }; 133 }; 134 135 config = mkIf cfg.enable { 136 environment.systemPackages = [ cfg.package ]; # for the CLI 137 systemd.packages = [ cfg.package ]; 138 systemd.services.tailscaled = { 139 after = lib.mkIf (config.networking.networkmanager.enable) [ "NetworkManager-wait-online.service" ]; 140 wantedBy = [ "multi-user.target" ]; 141 path = [ 142 (builtins.dirOf config.security.wrapperDir) # for `su` to use taildrive with correct access rights 143 pkgs.procps # for collecting running services (opt-in feature) 144 pkgs.getent # for `getent` to look up user shells 145 pkgs.kmod # required to pass tailscale's v6nat check 146 ] ++ lib.optional config.networking.resolvconf.enable config.networking.resolvconf.package; 147 serviceConfig.Environment = 148 [ 149 "PORT=${toString cfg.port}" 150 ''"FLAGS=--tun ${lib.escapeShellArg cfg.interfaceName} ${lib.concatStringsSep " " cfg.extraDaemonFlags}"'' 151 ] 152 ++ (lib.optionals (cfg.permitCertUid != null) [ 153 "TS_PERMIT_CERT_UID=${cfg.permitCertUid}" 154 ]) 155 ++ (lib.optionals (cfg.disableTaildrop) [ 156 "TS_DISABLE_TAILDROP=true" 157 ]); 158 # Restart tailscaled with a single `systemctl restart` at the 159 # end of activation, rather than a `stop` followed by a later 160 # `start`. Activation over Tailscale can hang for tens of 161 # seconds in the stop+start setup, if the activation script has 162 # a significant delay between the stop and start phases 163 # (e.g. script blocked on another unit with a slow shutdown). 164 # 165 # Tailscale is aware of the correctness tradeoff involved, and 166 # already makes its upstream systemd unit robust against unit 167 # version mismatches on restart for compatibility with other 168 # linux distros. 169 stopIfChanged = false; 170 }; 171 172 systemd.services.tailscaled-autoconnect = mkIf (cfg.authKeyFile != null) { 173 after = [ "tailscaled.service" ]; 174 wants = [ "tailscaled.service" ]; 175 wantedBy = [ "multi-user.target" ]; 176 serviceConfig = { 177 Type = "oneshot"; 178 }; 179 # https://github.com/tailscale/tailscale/blob/v1.72.1/ipn/backend.go#L24-L32 180 script = 181 let 182 statusCommand = "${lib.getExe cfg.package} status --json --peers=false | ${lib.getExe pkgs.jq} -r '.BackendState'"; 183 paramToString = v: if (builtins.isBool v) then (lib.boolToString v) else (toString v); 184 params = lib.pipe cfg.authKeyParameters [ 185 (lib.filterAttrs (_: v: v != null)) 186 (lib.mapAttrsToList (k: v: "${k}=${paramToString v}")) 187 (builtins.concatStringsSep "&") 188 (params: if params != "" then "?${params}" else "") 189 ]; 190 in 191 '' 192 while [[ "$(${statusCommand})" == "NoState" ]]; do 193 sleep 0.5 194 done 195 status=$(${statusCommand}) 196 if [[ "$status" == "NeedsLogin" || "$status" == "NeedsMachineAuth" ]]; then 197 ${lib.getExe cfg.package} up --auth-key "$(cat ${cfg.authKeyFile})${params}" ${escapeShellArgs cfg.extraUpFlags} 198 fi 199 ''; 200 }; 201 202 systemd.services.tailscaled-set = mkIf (cfg.extraSetFlags != [ ]) { 203 after = [ "tailscaled.service" ]; 204 wants = [ "tailscaled.service" ]; 205 wantedBy = [ "multi-user.target" ]; 206 serviceConfig = { 207 Type = "oneshot"; 208 }; 209 script = '' 210 ${lib.getExe cfg.package} set ${escapeShellArgs cfg.extraSetFlags} 211 ''; 212 }; 213 214 boot.kernel.sysctl = mkIf (cfg.useRoutingFeatures == "server" || cfg.useRoutingFeatures == "both") { 215 "net.ipv4.conf.all.forwarding" = mkOverride 97 true; 216 "net.ipv6.conf.all.forwarding" = mkOverride 97 true; 217 }; 218 219 networking.firewall.allowedUDPPorts = mkIf cfg.openFirewall [ cfg.port ]; 220 221 networking.firewall.checkReversePath = mkIf ( 222 cfg.useRoutingFeatures == "client" || cfg.useRoutingFeatures == "both" 223 ) "loose"; 224 225 networking.dhcpcd.denyInterfaces = [ cfg.interfaceName ]; 226 227 systemd.network.networks."50-tailscale" = mkIf isNetworkd { 228 matchConfig = { 229 Name = cfg.interfaceName; 230 }; 231 linkConfig = { 232 Unmanaged = true; 233 ActivationPolicy = "manual"; 234 }; 235 }; 236 }; 237}