1 { config, lib, pkgs, ... }:
6 cfg = config.services.tailscale;
7 isNetworkd = config.networking.useNetworkd;
9 meta.maintainers = with maintainers; [ mbaillie mfrw ];
11 options.services.tailscale = {
12 enable = mkEnableOption "Tailscale client daemon";
17 description = "The port to listen on for tunnel traffic (0=autoselect).";
20 interfaceName = mkOption {
22 default = "tailscale0";
23 description = ''The interface name for tunnel traffic. Use "userspace-networking" (beta) to not use TUN.'';
26 permitCertUid = mkOption {
27 type = types.nullOr types.nonEmptyStr;
29 description = "Username or user ID of the user allowed to to fetch Tailscale TLS certificates for the node.";
32 package = lib.mkPackageOption pkgs "tailscale" {};
34 openFirewall = mkOption {
37 description = "Whether to open the firewall for the specified port.";
40 useRoutingFeatures = mkOption {
41 type = types.enum [ "none" "client" "server" "both" ];
45 Enables settings required for Tailscale's routing features like subnet routers and exit nodes.
47 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`.
49 When set to `client` or `both`, reverse path filtering will be set to loose instead of strict.
50 When set to `server` or `both`, IP forwarding will be enabled.
54 authKeyFile = mkOption {
55 type = types.nullOr types.path;
57 example = "/run/secrets/tailscale_key";
59 A file containing the auth key.
63 authKeyParameters = mkOption {
64 type = types.submodule {
66 ephemeral = mkOption {
67 type = types.nullOr types.bool;
69 description = "Whether to register as an ephemeral node.";
71 preauthorized = mkOption {
72 type = types.nullOr types.bool;
74 description = "Whether to skip manual device approval.";
77 type = types.nullOr types.str;
79 description = "Base URL for the Tailscale API.";
85 Extra parameters to pass after the auth key.
86 See https://tailscale.com/kb/1215/oauth-clients#registering-new-nodes-using-oauth-credentials
90 extraUpFlags = mkOption {
92 Extra flags to pass to {command}`tailscale up`. Only applied if `authKeyFile` is specified.";
94 type = types.listOf types.str;
99 extraSetFlags = mkOption {
100 description = "Extra flags to pass to {command}`tailscale set`.";
101 type = types.listOf types.str;
103 example = ["--advertise-exit-node"];
106 extraDaemonFlags = mkOption {
107 description = "Extra flags to pass to {command}`tailscaled`.";
108 type = types.listOf types.str;
110 example = ["--no-logs-no-support"];
114 config = mkIf cfg.enable {
115 environment.systemPackages = [ cfg.package ]; # for the CLI
116 systemd.packages = [ cfg.package ];
117 systemd.services.tailscaled = {
118 after = lib.mkIf (config.networking.networkmanager.enable) [ "NetworkManager-wait-online.service" ];
119 wantedBy = [ "multi-user.target" ];
121 (builtins.dirOf config.security.wrapperDir) # for `su` to use taildrive with correct access rights
122 pkgs.procps # for collecting running services (opt-in feature)
123 pkgs.getent # for `getent` to look up user shells
124 pkgs.kmod # required to pass tailscale's v6nat check
125 ] ++ lib.optional config.networking.resolvconf.enable config.networking.resolvconf.package;
126 serviceConfig.Environment = [
127 "PORT=${toString cfg.port}"
128 ''"FLAGS=--tun ${lib.escapeShellArg cfg.interfaceName} ${lib.concatStringsSep " " cfg.extraDaemonFlags}"''
129 ] ++ (lib.optionals (cfg.permitCertUid != null) [
130 "TS_PERMIT_CERT_UID=${cfg.permitCertUid}"
132 # Restart tailscaled with a single `systemctl restart` at the
133 # end of activation, rather than a `stop` followed by a later
134 # `start`. Activation over Tailscale can hang for tens of
135 # seconds in the stop+start setup, if the activation script has
136 # a significant delay between the stop and start phases
137 # (e.g. script blocked on another unit with a slow shutdown).
139 # Tailscale is aware of the correctness tradeoff involved, and
140 # already makes its upstream systemd unit robust against unit
141 # version mismatches on restart for compatibility with other
143 stopIfChanged = false;
146 systemd.services.tailscaled-autoconnect = mkIf (cfg.authKeyFile != null) {
147 after = ["tailscaled.service"];
148 wants = ["tailscaled.service"];
149 wantedBy = [ "multi-user.target" ];
153 # https://github.com/tailscale/tailscale/blob/v1.72.1/ipn/backend.go#L24-L32
155 statusCommand = "${lib.getExe cfg.package} status --json --peers=false | ${lib.getExe pkgs.jq} -r '.BackendState'";
157 if (builtins.isBool v) then (lib.boolToString v)
159 params = lib.pipe cfg.authKeyParameters [
160 (lib.filterAttrs (_: v: v != null))
161 (lib.mapAttrsToList (k: v: "${k}=${paramToString v}"))
162 (builtins.concatStringsSep "&")
163 (params: if params != "" then "?${params}" else "")
166 while [[ "$(${statusCommand})" == "NoState" ]]; do
169 status=$(${statusCommand})
170 if [[ "$status" == "NeedsLogin" || "$status" == "NeedsMachineAuth" ]]; then
171 ${lib.getExe cfg.package} up --auth-key "$(cat ${cfg.authKeyFile})${params}" ${escapeShellArgs cfg.extraUpFlags}
176 systemd.services.tailscaled-set = mkIf (cfg.extraSetFlags != []) {
177 after = ["tailscaled.service"];
178 wants = ["tailscaled.service"];
179 wantedBy = [ "multi-user.target" ];
184 ${lib.getExe cfg.package} set ${escapeShellArgs cfg.extraSetFlags}
188 boot.kernel.sysctl = mkIf (cfg.useRoutingFeatures == "server" || cfg.useRoutingFeatures == "both") {
189 "net.ipv4.conf.all.forwarding" = mkOverride 97 true;
190 "net.ipv6.conf.all.forwarding" = mkOverride 97 true;
193 networking.firewall.allowedUDPPorts = mkIf cfg.openFirewall [ cfg.port ];
195 networking.firewall.checkReversePath = mkIf (cfg.useRoutingFeatures == "client" || cfg.useRoutingFeatures == "both") "loose";
197 networking.dhcpcd.denyInterfaces = [ cfg.interfaceName ];
199 systemd.network.networks."50-tailscale" = mkIf isNetworkd {
201 Name = cfg.interfaceName;
205 ActivationPolicy = "manual";