From e093c8c9a59b20797ea2e1c7ac492201e8d38eb5 Mon Sep 17 00:00:00 2001 From: Mike O'Driscoll Date: Tue, 29 Jul 2025 21:33:06 -0400 Subject: [PATCH] Add tailscale module. --- flake.nix | 4 +- nixos/tailscaled-module.nix | 192 +++++++++++++++++++++++++++++++++++- 2 files changed, 193 insertions(+), 3 deletions(-) diff --git a/flake.nix b/flake.nix index b6dc9b7de..a3b419e5f 100644 --- a/flake.nix +++ b/flake.nix @@ -123,9 +123,9 @@ }; nixosModules = { - # tailscale = import ./nixos/tailscaled-module.nix; + tailscale = import ./nixos/tailscaled-module.nix self; tsidp = import ./nixos/tsidp-module.nix self; - # default = self.nixosModules.tailscale; + default = self.nixosModules.tailscale; }; devShells = eachSystem (pkgs: { diff --git a/nixos/tailscaled-module.nix b/nixos/tailscaled-module.nix index 6a9e1395a..a5bf0b06d 100644 --- a/nixos/tailscaled-module.nix +++ b/nixos/tailscaled-module.nix @@ -1,4 +1,4 @@ -{ +self: { config, lib, ... @@ -16,10 +16,200 @@ in { options.services.tailscale = { enable = mkEnabledOption "Enable Tailscale service"; + package = mkOption { + type = types.package; + default = self.packages.${pkgs.system}.tailscale; + description = "The Tailscale package to use."; + }; + port = mkOption { type = types.port; default = 41641; description = "The port Tailscale listens on."; }; + + interface = mkOption { + type = types.str; + default = "tailscale0"; + description = "The network interface Tailscale uses."; + }; + + permitCertUid = mkOption { + type = types.nullOr types.nonEmptyStr; + default = null; + description = "Username or UID allowed to fetch tailnet TLS certificates"; + }; + + disableTaildrop = mkOption { + type = types.bool; + default = false; + description = "Disable Tailscale Taildrop feature."; + }; + + openFirewall = mkOption { + type = types.bool; + default = false; + description = "Open the firewall for Tailscale traffic. Recommended true to allow for direct connections."; + }; + + useRoutingFeatures = mkOption { + type = types.enum [ + "none" + "client" + "server" + "both" + ]; + default = "none"; + example = "server"; + description = '' + Enables settings required for Tailscale's routing features like subnet routers and exit nodes. + + 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`. + + When set to `client` or `both`, reverse path filtering will be set to loose instead of strict. + When set to `server` or `both`, IP forwarding will be enabled allowing proper packet forwarding for exit node or subnet router functionality. + + See https://tailscale.com/kb/1019/subnets#enable-ip-forwarding for packet forwarding + See https://github.com/tailscale/tailscale/issues/3310 for reverse path filtering + ''; + }; + + authKeyFile = mkOption { + type = types.nullOr types.path; + default = null; + example = "/run/secrets/tailscale_key"; + description = '' + Path to a file containing a Tailscale auth key. If set, this will be used to automatically authenticate the Tailscale client. + The file should contain a single line with the auth key. + This is useful for automated setups where you want to avoid manual authentication. + ''; + }; + + extraUpFlags = mkOption { + description = '' + Extra flags to pass to {command}`tailscale up`. Only applied if `authKeyFile` is specified."; + ''; + type = types.listOf types.str; + default = []; + example = ["--ssh" "--accept-routes"]; + }; + + extraSetFlags = mkOption { + description = "Extra flags to pass to {command}`tailscale set`."; + type = types.listOf types.str; + default = []; + example = ["--advertise-exit-node" "--shields-up"]; + }; + + extraDaemonFlags = mkOption { + description = "Extra flags to pass to {command}`tailscaled`."; + type = types.listOf types.str; + default = []; + example = ["--no-logs-no-support" "-encrypt-state"]; + }; + + RuntimeDirectory = mkOption { + type = types.str; + default = "tailscale"; + description = "The runtime directory for Tailscale. This is where Tailscale will store its state."; + }; + + StateDirectory = mkOption { + type = types.str; + default = "tailscale"; + description = "The state directory for Tailscale. This is where Tailscale will store its persistent state."; + }; + + CacheDirectory = mkOption { + type = types.str; + default = "tailscale"; + description = "The cache directory for Tailscale. This is where Tailscale will store its cache."; + }; + + Cleanup = mkOption { + type = types.bool; + default = true; + description = "Whether to clean up Tailscale state on post stop."; + }; + }; + + config = mkIf cfg.enable { + environment.systemPackages = [cfg.package]; + + boot.kernel.sysctl = mkIf (cfg.useRoutingFeatures == "server" || cfg.useRoutingFeatures == "both") { + "net.ipv4.conf.all.forwarding" = mkOverride 97 true; + "net.ipv6.conf.all.forwarding" = mkOverride 97 true; + }; + + networking.firewall.allowedUDPPorts = mkIf cfg.openFirewall [cfg.port]; + + networking.firewall.checkReversePath = mkIf ( + cfg.useRoutingFeatures == "client" || cfg.useRoutingFeatures == "both" + ) "loose"; + + networking.dhcpcd.denyInterfaces = [cfg.interfaceName]; + + systemd.network.networks."50-tailscale" = mkIf config.networking.useNetworkd { + matchConfig = { + Name = cfg.interfaceName; + }; + linkConfig = { + Unmanaged = true; + ActivationPolicy = "manual"; + }; + }; + + systemd.packages = [cfg.package]; + systemd.services.tailscaled = { + wantedBy = ["multi-user.target"]; + wants = ["network-pre.target"]; + after = ["network-pre.target" "NetworkManager.service" "systemd-resolved.service"]; + path = + [ + (builtins.dirOf config.security.wrapperDir) + pkgs.iproute2 + pkgs.procps + pkgs.getent + pkgs.shadow + ] + ++ lib.optional config.networking.resolvconf.enable config.networking.resolvconf.package; + + serviceConfig = { + Environment = + [ + "PORT=${toString cfg.port}" + ''"FLAGS=--tun ${lib.escapeShellArg cfg.interfaceName} ${lib.concatStringsSep " " cfg.extraDaemonFlags}" ${lib.optionalString (cfg.authKeyFile != null) " --auth-key file:${cfg.authKeyFile}"}'' + ] + ++ (lib.optionals (cfg.permitCertUid != null) [ + "TS_PERMIT_CERT_UID=${cfg.permitCertUid}" + ]) + ++ (lib.optionals (cfg.disableTaildrop) [ + "TS_DISABLE_TAILDROP=true" + ]); + + Restart = "on-failure"; + StateDirectory = cfg.StateDirectory; + StateDirectoryMode = "0700"; + RuntimeDirectory = cfg.RuntimeDirectory; + RuntimeDirectoryMode = "0755"; + CacheDirectory = cfg.CacheDirectory; + CacheDirectoryMode = "0750"; + Type = "notify"; + }; + + stopIfChanged = false; + }; + + systemd.services.tailscaled-set = mkIf (cfg.extraSetFlags != []) { + after = ["tailscaled.service"]; + wants = ["tailscaled.service"]; + wantedBy = ["multi-user.target"]; + serviceConfig = { + Type = "oneshot"; + }; + script = '' + ${lib.getExe cfg.package} set ${escapeShellArgs cfg.extraSetFlags} + ''; + }; }; }