1{ config, lib, pkgs, ... }:
2
3with lib;
4
5let
6 cfg = config.services.tailscale;
7 isNetworkd = config.networking.useNetworkd;
8in {
9 meta.maintainers = with maintainers; [ mbaillie mfrw ];
10
11 options.services.tailscale = {
12 enable = mkEnableOption "Tailscale client daemon";
13
14 port = mkOption {
15 type = types.port;
16 default = 41641;
17 description = "The port to listen on for tunnel traffic (0=autoselect).";
18 };
19
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 };
25
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 };
31
32 package = lib.mkPackageOption pkgs "tailscale" {};
33
34 openFirewall = mkOption {
35 default = false;
36 type = types.bool;
37 description = "Whether to open the firewall for the specified port.";
38 };
39
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.
46
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`.
48
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 };
53
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 };
62
63 extraUpFlags = mkOption {
64 description = "Extra flags to pass to {command}`tailscale up`.";
65 type = types.listOf types.str;
66 default = [];
67 example = ["--ssh"];
68 };
69
70 extraDaemonFlags = mkOption {
71 description = "Extra flags to pass to {command}`tailscaled`.";
72 type = types.listOf types.str;
73 default = [];
74 example = ["--no-logs-no-support"];
75 };
76 };
77
78 config = mkIf cfg.enable {
79 environment.systemPackages = [ cfg.package ]; # for the CLI
80 systemd.packages = [ cfg.package ];
81 systemd.services.tailscaled = {
82 wantedBy = [ "multi-user.target" ];
83 path = [
84 pkgs.procps # for collecting running services (opt-in feature)
85 pkgs.getent # for `getent` to look up user shells
86 pkgs.kmod # required to pass tailscale's v6nat check
87 ] ++ lib.optional config.networking.resolvconf.enable config.networking.resolvconf.package;
88 serviceConfig.Environment = [
89 "PORT=${toString cfg.port}"
90 ''"FLAGS=--tun ${lib.escapeShellArg cfg.interfaceName} ${lib.concatStringsSep " " cfg.extraDaemonFlags}"''
91 ] ++ (lib.optionals (cfg.permitCertUid != null) [
92 "TS_PERMIT_CERT_UID=${cfg.permitCertUid}"
93 ]);
94 # Restart tailscaled with a single `systemctl restart` at the
95 # end of activation, rather than a `stop` followed by a later
96 # `start`. Activation over Tailscale can hang for tens of
97 # seconds in the stop+start setup, if the activation script has
98 # a significant delay between the stop and start phases
99 # (e.g. script blocked on another unit with a slow shutdown).
100 #
101 # Tailscale is aware of the correctness tradeoff involved, and
102 # already makes its upstream systemd unit robust against unit
103 # version mismatches on restart for compatibility with other
104 # linux distros.
105 stopIfChanged = false;
106 };
107
108 systemd.services.tailscaled-autoconnect = mkIf (cfg.authKeyFile != null) {
109 after = ["tailscaled.service"];
110 wants = ["tailscaled.service"];
111 wantedBy = [ "multi-user.target" ];
112 serviceConfig = {
113 Type = "oneshot";
114 };
115 script = ''
116 status=$(${config.systemd.package}/bin/systemctl show -P StatusText tailscaled.service)
117 if [[ $status != Connected* ]]; then
118 ${cfg.package}/bin/tailscale up --auth-key 'file:${cfg.authKeyFile}' ${escapeShellArgs cfg.extraUpFlags}
119 fi
120 '';
121 };
122
123 boot.kernel.sysctl = mkIf (cfg.useRoutingFeatures == "server" || cfg.useRoutingFeatures == "both") {
124 "net.ipv4.conf.all.forwarding" = mkOverride 97 true;
125 "net.ipv6.conf.all.forwarding" = mkOverride 97 true;
126 };
127
128 networking.firewall.allowedUDPPorts = mkIf cfg.openFirewall [ cfg.port ];
129
130 networking.firewall.checkReversePath = mkIf (cfg.useRoutingFeatures == "client" || cfg.useRoutingFeatures == "both") "loose";
131
132 networking.dhcpcd.denyInterfaces = [ cfg.interfaceName ];
133
134 systemd.network.networks."50-tailscale" = mkIf isNetworkd {
135 matchConfig = {
136 Name = cfg.interfaceName;
137 };
138 linkConfig = {
139 Unmanaged = true;
140 ActivationPolicy = "manual";
141 };
142 };
143 };
144}