From 7e28275cfbae6f34250998347d0f0f887715ef6d Mon Sep 17 00:00:00 2001 From: Mike O'Driscoll Date: Wed, 23 Jul 2025 11:41:05 -0400 Subject: [PATCH] flake.nix,nixos/: add tailscale module Pull in base upstream NixOS module to allow for deploying updated Tailscale from this flake. Fixes #17678 Signed-off-by: Mike O'Driscoll --- flake.nix | 19 +++- nixos/tailscaled-module.nix | 218 ++++++++++++++++++++++++++++++++++++ 2 files changed, 236 insertions(+), 1 deletion(-) create mode 100644 nixos/tailscaled-module.nix diff --git a/flake.nix b/flake.nix index 726757f7a..82ea1a12b 100644 --- a/flake.nix +++ b/flake.nix @@ -1,3 +1,5 @@ +# Copyright (c) Tailscale Inc & AUTHORS +# SPDX-License-Identifier: BSD-3-Clause # flake.nix describes a Nix source repository that provides # development builds of Tailscale and the fork of the Go compiler # toolchain that Tailscale maintains. It also provides a development @@ -67,7 +69,12 @@ }) ]; })); + + # tailscaleRev is the git commit at which this flake was imported, + # or the empty string when building from a local checkout of the + # tailscale repo. tailscaleRev = self.rev or ""; + lib = nixpkgs.lib; in { # tailscale takes a nixpkgs package set, and builds Tailscale from # the same commit as this flake. IOW, it provides "tailscale built @@ -90,6 +97,8 @@ default = pkgs.buildGo125Module { name = "tailscale"; pname = "tailscale"; + meta.mainProgram = "tailscaled"; + src = ./.; vendorHash = pkgs.lib.fileContents ./go.mod.sri; nativeBuildInputs = [pkgs.makeWrapper pkgs.installShellFiles]; @@ -98,7 +107,6 @@ subPackages = [ "cmd/tailscale" "cmd/tailscaled" - "cmd/tsidp" ]; doCheck = false; @@ -131,6 +139,15 @@ tailscale = default; }); + overlays.default = final: prev: { + tailscale = self.packages.${prev.stdenv.hostPlatform.system}.default; + }; + + nixosModules = { + tailscale = import ./nixos/tailscaled-module.nix self; + default = self.nixosModules.tailscale; + }; + devShells = eachSystem (pkgs: { devShell = pkgs.mkShell { packages = with pkgs; [ diff --git a/nixos/tailscaled-module.nix b/nixos/tailscaled-module.nix new file mode 100644 index 000000000..1f9f62585 --- /dev/null +++ b/nixos/tailscaled-module.nix @@ -0,0 +1,218 @@ +# Copyright (c) 2003-2025 Eelco Dolstra and the Nixpkgs/NixOS contributors +# Copyright (c) Tailscale Inc & AUTHORS +# SPDX-License-Identifier: MIT +self: { + config, + lib, + ... +}: let + cfg = config.services.tailscale; + inherit + (lib) + mkEnableOption + mkIf + mkOption + types + ; +in { + # Tailscale config options + 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} + ''; + }; + }; +}