Unverified Commit 309df1f2 authored by Fernando Rodrigues's avatar Fernando Rodrigues Committed by GitHub
Browse files

nixos/tailscale-serve: init declarative Tailscale services module (#482230)

parents 307e2d16 b463561b
Loading
Loading
Loading
Loading
+2 −0
Original line number Diff line number Diff line
@@ -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).
+1 −0
Original line number Diff line number Diff line
@@ -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
+145 −0
Original line number Diff line number Diff line
{
  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 <https://tailscale.com/kb/1589/tailscale-services-configuration-file> 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 `<protocol>:<port>` or `<protocol>:<port-range>`.
                Currently only `tcp` protocol is supported.

                Values should be in the format `<protocol>://<host:port>` 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 <https://tailscale.com/kb/1589/tailscale-services-configuration-file> 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}";
      };
    };
  };
}