From b463561b2aa1bdadadd941db46d2c4f667cd6fa3 Mon Sep 17 00:00:00 2001 From: Bouke van der Bijl Date: Wed, 21 Jan 2026 11:14:20 +0100 Subject: [PATCH] nixos/tailscale-serve: init Tailscale serve module --- .../manual/release-notes/rl-2605.section.md | 2 + nixos/modules/module-list.nix | 1 + .../services/networking/tailscale-serve.nix | 145 ++++++++++++++++++ 3 files changed, 148 insertions(+) create mode 100644 nixos/modules/services/networking/tailscale-serve.nix diff --git a/nixos/doc/manual/release-notes/rl-2605.section.md b/nixos/doc/manual/release-notes/rl-2605.section.md index d29cb9aa49dc..3f200e3eba94 100644 --- a/nixos/doc/manual/release-notes/rl-2605.section.md +++ b/nixos/doc/manual/release-notes/rl-2605.section.md @@ -20,6 +20,8 @@ - [reaction](https://reaction.ppom.me/), a daemon that scans program outputs for repeated patterns, and takes action. A common usage is to scan ssh and webserver logs, and to ban hosts that cause multiple authentication errors. A modern alternative to fail2ban. Available as [services.reaction](#opt-services.reaction.enable). +- [Tailscale Serve](https://tailscale.com/kb/1552/tailscale-services), configure Tailscale Serve for exposing local services to your tailnet. Available as [services.tailscale.serve](#opt-services.tailscale.serve.enable). + - [qui](https://github.com/autobrr/qui), a modern alternative webUI for qBittorrent, with multi-instance support. Written in Go/React. Available as [services.qui](#opt-services.qui.enable). - [LibreChat](https://www.librechat.ai/), open-source self-hostable ChatGPT clone with Agents and RAG APIs. Available as [services.librechat](#opt-services.librechat.enable). diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index bec540d6f504..1bf89a1c1f67 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -1391,6 +1391,7 @@ ./services/networking/syncthing.nix ./services/networking/tailscale-auth.nix ./services/networking/tailscale-derper.nix + ./services/networking/tailscale-serve.nix ./services/networking/tailscale.nix ./services/networking/tayga.nix ./services/networking/tcpcrypt.nix diff --git a/nixos/modules/services/networking/tailscale-serve.nix b/nixos/modules/services/networking/tailscale-serve.nix new file mode 100644 index 000000000000..87ee4026c1c4 --- /dev/null +++ b/nixos/modules/services/networking/tailscale-serve.nix @@ -0,0 +1,145 @@ +{ + config, + lib, + pkgs, + ... +}: + +let + cfg = config.services.tailscale.serve; + settingsFormat = pkgs.formats.json { }; + + # Build the serve config structure with svc: prefix on service names + serveConfig = { + version = "0.0.1"; + services = lib.mapAttrs' ( + name: serviceCfg: + lib.nameValuePair "svc:${name}" ( + { + endpoints = serviceCfg.endpoints; + } + // lib.optionalAttrs (serviceCfg.advertised != null) { + inherit (serviceCfg) advertised; + } + ) + ) cfg.services; + }; + + configFile = + if cfg.configFile != null then + cfg.configFile + else + settingsFormat.generate "tailscale-serve-config.json" serveConfig; +in +{ + meta.maintainers = with lib.maintainers; [ + bouk + ]; + + options.services.tailscale.serve = { + enable = lib.mkEnableOption "Tailscale Serve configuration"; + + configFile = lib.mkOption { + type = lib.types.nullOr lib.types.path; + default = null; + description = '' + Path to a Tailscale Serve configuration file in JSON format. + If set, this takes precedence over {option}`services.tailscale.serve.services`. + + See for the configuration format. + ''; + example = "/run/secrets/tailscale-serve.json"; + }; + + services = lib.mkOption { + type = lib.types.attrsOf ( + lib.types.submodule { + options = { + endpoints = lib.mkOption { + type = lib.types.attrsOf lib.types.str; + description = '' + Map of incoming traffic patterns to local targets. + + Keys should be in the format `:` or `:`. + Currently only `tcp` protocol is supported. + + Values should be in the format `://` where protocol + is `http`, `https`, or `tcp`. + ''; + example = { + "tcp:443" = "https://localhost:443"; + "tcp:8080" = "http://localhost:8080"; + }; + }; + + advertised = lib.mkOption { + type = lib.types.nullOr lib.types.bool; + default = null; + description = '' + Whether the service should accept new connections. + Defaults to `true` when not specified. + ''; + }; + }; + } + ); + default = { }; + description = '' + Services to configure for Tailscale Serve. + + Each attribute name should be the service name (without the `svc:` prefix). + The `svc:` prefix will be added automatically. + + See for details. + ''; + example = lib.literalExpression '' + { + web-server = { + endpoints = { + "tcp:443" = "https://localhost:443"; + }; + }; + api = { + endpoints = { + "tcp:8080" = "http://localhost:8080"; + }; + advertised = true; + }; + } + ''; + }; + }; + + config = lib.mkIf cfg.enable { + assertions = [ + { + assertion = config.services.tailscale.enable; + message = "services.tailscale.serve requires services.tailscale.enable to be true"; + } + { + assertion = cfg.configFile != null || cfg.services != { }; + message = "services.tailscale.serve requires either configFile or services to be set"; + } + ]; + + systemd.services.tailscale-serve = { + description = "Tailscale Serve Configuration"; + + after = [ + "tailscaled.service" + "tailscaled-autoconnect.service" + "tailscaled-set.service" + ]; + wants = [ "tailscaled.service" ]; + wantedBy = [ "multi-user.target" ]; + + restartTriggers = [ configFile ]; + + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + ExecStart = "${lib.getExe config.services.tailscale.package} serve set-config --all ${configFile}"; + }; + }; + }; +}