vuls: init at 0.27.0
[NixPkgs.git] / nixos / modules / services / networking / tailscale.nix
blob859da5be81dd8c76857d2b128d17e5c6b13dcebb
1 { config, lib, pkgs, ... }:
3 with lib;
5 let
6   cfg = config.services.tailscale;
7   isNetworkd = config.networking.useNetworkd;
8 in {
9   meta.maintainers = with maintainers; [ mbaillie mfrw ];
11   options.services.tailscale = {
12     enable = mkEnableOption "Tailscale client daemon";
14     port = mkOption {
15       type = types.port;
16       default = 41641;
17       description = "The port to listen on for tunnel traffic (0=autoselect).";
18     };
20     interfaceName = mkOption {
21       type = types.str;
22       default = "tailscale0";
23       description = ''The interface name for tunnel traffic. Use "userspace-networking" (beta) to not use TUN.'';
24     };
26     permitCertUid = mkOption {
27       type = types.nullOr types.nonEmptyStr;
28       default = null;
29       description = "Username or user ID of the user allowed to to fetch Tailscale TLS certificates for the node.";
30     };
32     package = lib.mkPackageOption pkgs "tailscale" {};
34     openFirewall = mkOption {
35       default = false;
36       type = types.bool;
37       description = "Whether to open the firewall for the specified port.";
38     };
40     useRoutingFeatures = mkOption {
41       type = types.enum [ "none" "client" "server" "both" ];
42       default = "none";
43       example = "server";
44       description = ''
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.
51       '';
52     };
54     authKeyFile = mkOption {
55       type = types.nullOr types.path;
56       default = null;
57       example = "/run/secrets/tailscale_key";
58       description = ''
59         A file containing the auth key.
60       '';
61     };
63     authKeyParameters = mkOption {
64       type = types.submodule {
65         options = {
66           ephemeral = mkOption {
67             type = types.nullOr types.bool;
68             default = null;
69             description = "Whether to register as an ephemeral node.";
70           };
71           preauthorized = mkOption {
72             type = types.nullOr types.bool;
73             default = null;
74             description = "Whether to skip manual device approval.";
75           };
76           baseURL = mkOption {
77             type = types.nullOr types.str;
78             default = null;
79             description = "Base URL for the Tailscale API.";
80           };
81         };
82       };
83       default = { };
84       description = ''
85         Extra parameters to pass after the auth key.
86         See https://tailscale.com/kb/1215/oauth-clients#registering-new-nodes-using-oauth-credentials
87       '';
88     };
90     extraUpFlags = mkOption {
91       description = ''
92         Extra flags to pass to {command}`tailscale up`. Only applied if `authKeyFile` is specified.";
93       '';
94       type = types.listOf types.str;
95       default = [];
96       example = ["--ssh"];
97     };
99     extraSetFlags = mkOption {
100       description = "Extra flags to pass to {command}`tailscale set`.";
101       type = types.listOf types.str;
102       default = [];
103       example = ["--advertise-exit-node"];
104     };
106     extraDaemonFlags = mkOption {
107       description = "Extra flags to pass to {command}`tailscaled`.";
108       type = types.listOf types.str;
109       default = [];
110       example = ["--no-logs-no-support"];
111     };
112   };
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" ];
120       path = [
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}"
131       ]);
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).
138       #
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
142       # linux distros.
143       stopIfChanged = false;
144     };
146     systemd.services.tailscaled-autoconnect = mkIf (cfg.authKeyFile != null) {
147       after = ["tailscaled.service"];
148       wants = ["tailscaled.service"];
149       wantedBy = [ "multi-user.target" ];
150       serviceConfig = {
151         Type = "oneshot";
152       };
153       # https://github.com/tailscale/tailscale/blob/v1.72.1/ipn/backend.go#L24-L32
154       script = let
155         statusCommand = "${lib.getExe cfg.package} status --json --peers=false | ${lib.getExe pkgs.jq} -r '.BackendState'";
156         paramToString = v:
157           if (builtins.isBool v) then (lib.boolToString v)
158           else (toString 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 "")
164         ];
165       in ''
166         while [[ "$(${statusCommand})" == "NoState" ]]; do
167           sleep 0.5
168         done
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}
172         fi
173       '';
174     };
176     systemd.services.tailscaled-set = mkIf (cfg.extraSetFlags != []) {
177       after = ["tailscaled.service"];
178       wants = ["tailscaled.service"];
179       wantedBy = [ "multi-user.target" ];
180       serviceConfig = {
181         Type = "oneshot";
182       };
183       script = ''
184         ${lib.getExe cfg.package} set ${escapeShellArgs cfg.extraSetFlags}
185       '';
186     };
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;
191     };
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 {
200       matchConfig = {
201         Name = cfg.interfaceName;
202       };
203       linkConfig = {
204         Unmanaged = true;
205         ActivationPolicy = "manual";
206       };
207     };
208   };