mirror of
https://github.com/tailscale/tailscale.git
synced 2026-05-05 12:16:44 +02:00
nixos: add Tailscale Services support
Add a `services` option to the per-instance submodule for declarative Tailscale Services configuration. Service names are specified without the `svc:` prefix (added automatically in the generated JSON). The module generates a JSON config file matching the v0.0.1 spec and applies it via a oneshot systemd service that runs: tailscale serve set-config --all <config> tailscale serve advertise svc:<name> # for each service The `advertised` field uses nullOr bool to match the Go opt.Bool semantics: omitted from JSON when unset (meaning advertised by default), included only when explicitly set. The serve-config service orders after both the daemon and autoconnect services, polls until tailscaled is ready, uses restartTriggers to re-apply on config changes, and enables strict shell checks.
This commit is contained in:
parent
08f84bad46
commit
75719c4fb0
@ -202,5 +202,65 @@ in {
|
||||
iptables rules, routes, and the TUN device.
|
||||
'';
|
||||
};
|
||||
|
||||
services = mkOption {
|
||||
type = types.attrsOf (types.submodule {
|
||||
options = {
|
||||
endpoints = mkOption {
|
||||
type = types.attrsOf types.str;
|
||||
description = ''
|
||||
Endpoint mappings for this service.
|
||||
|
||||
Keys use the format `"tcp:PORT"` or `"tcp:START-END"` for
|
||||
port ranges. Only TCP is supported as the transport protocol.
|
||||
|
||||
Values specify the local target using a URI:
|
||||
`"http://host:port"`, `"https://host:port"`,
|
||||
`"https+insecure://host:port"`, `"tcp://host:port"`,
|
||||
or `"tls-terminated-tcp://host:port"`.
|
||||
'';
|
||||
example = {
|
||||
"tcp:443" = "https://localhost:8443";
|
||||
"tcp:5432" = "tcp://localhost:5432";
|
||||
};
|
||||
};
|
||||
|
||||
advertised = mkOption {
|
||||
type = types.nullOr types.bool;
|
||||
default = null;
|
||||
description = ''
|
||||
Whether the service accepts new connections.
|
||||
When null (the default), the service is advertised.
|
||||
Set to false to configure endpoints without advertising.
|
||||
'';
|
||||
};
|
||||
};
|
||||
});
|
||||
default = {};
|
||||
description = ''
|
||||
Tailscale Services configuration. Service names are specified
|
||||
without the `svc:` prefix (it is added automatically).
|
||||
|
||||
When using `services.tailscales` (multi-instance), any services
|
||||
defined under `services.tailscale.services` are automatically
|
||||
merged into all plural instances as shared services. Per-instance
|
||||
services with the same name take precedence over shared ones.
|
||||
|
||||
Services must be pre-defined in the Tailscale admin console.
|
||||
The host must use tag-based identity.
|
||||
|
||||
See <https://tailscale.com/docs/features/tailscale-services>
|
||||
'';
|
||||
example = literalExpression ''
|
||||
{
|
||||
prometheus = {
|
||||
endpoints."tcp:443" = "http://localhost:9090";
|
||||
};
|
||||
postgres = {
|
||||
endpoints."tcp:5432" = "tcp://localhost:5432";
|
||||
};
|
||||
}
|
||||
'';
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@ -20,17 +20,21 @@ self: {
|
||||
}: let
|
||||
inherit (lib)
|
||||
any
|
||||
attrNames
|
||||
attrValues
|
||||
boolToString
|
||||
concatMap
|
||||
concatMapStringsSep
|
||||
concatStringsSep
|
||||
escapeShellArg
|
||||
escapeShellArgs
|
||||
filterAttrs
|
||||
mapAttrs'
|
||||
mapAttrsToList
|
||||
mkIf
|
||||
mkMerge
|
||||
mkOverride
|
||||
nameValuePair
|
||||
optional
|
||||
optionalAttrs
|
||||
optionals
|
||||
@ -258,7 +262,48 @@ self: {
|
||||
tailscale --socket=${escapeShellArg meta.socketPath} set ${escapeShellArgs cfg.extraSetFlags}
|
||||
'';
|
||||
};
|
||||
};
|
||||
}
|
||||
# ── Serve config service (when services are defined) ──
|
||||
// optionalAttrs (effServices != {}) (let
|
||||
configFile = mkServeConfigFile name effServices;
|
||||
socket = escapeShellArg meta.socketPath;
|
||||
svcNames = attrNames effServices;
|
||||
in {
|
||||
"${meta.svcName}-serve-config" = {
|
||||
description =
|
||||
"Tailscale serve config"
|
||||
+ optionalString (!meta.isDefault) " (${name})";
|
||||
after = [
|
||||
"${meta.svcName}.service"
|
||||
"${meta.svcName}-autoconnect.service"
|
||||
];
|
||||
wants = ["${meta.svcName}.service"];
|
||||
wantedBy = ["multi-user.target"];
|
||||
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
RemainAfterExit = true;
|
||||
};
|
||||
|
||||
path = [cfg.package];
|
||||
|
||||
enableStrictShellChecks = true;
|
||||
|
||||
restartTriggers = [configFile];
|
||||
|
||||
script = ''
|
||||
until tailscale --socket=${socket} status &>/dev/null; do
|
||||
sleep 1
|
||||
done
|
||||
|
||||
tailscale --socket=${socket} serve set-config --all ${configFile}
|
||||
|
||||
${concatMapStringsSep "\n" (svcName:
|
||||
"tailscale --socket=${socket} serve advertise svc:${escapeShellArg svcName}")
|
||||
svcNames}
|
||||
'';
|
||||
};
|
||||
});
|
||||
|
||||
# Generate a CLI wrapper package for a named instance.
|
||||
mkCliWrapper = name: cfg: let
|
||||
@ -269,6 +314,25 @@ self: {
|
||||
exec ${cfg.package}/bin/tailscale --socket=${escapeShellArg meta.socketPath} "$@"
|
||||
'';
|
||||
};
|
||||
|
||||
# Generate the JSON config file for tailscale serve set-config --all.
|
||||
# Prepends "svc:" to each service name and omits `advertised` when null
|
||||
# to match the Go opt.Bool semantics (unset = advertised by default).
|
||||
mkServeConfigFile = name: servicesAttr: let
|
||||
servicesJson = {
|
||||
version = "0.0.1";
|
||||
services = mapAttrs' (svcName: svcCfg:
|
||||
nameValuePair "svc:${svcName}" (
|
||||
{inherit (svcCfg) endpoints;}
|
||||
// optionalAttrs (svcCfg.advertised != null) {
|
||||
inherit (svcCfg) advertised;
|
||||
}
|
||||
))
|
||||
servicesAttr;
|
||||
};
|
||||
in
|
||||
pkgs.writeText "tailscale-services-${name}.json"
|
||||
(builtins.toJSON servicesJson);
|
||||
in {
|
||||
config = mkMerge [
|
||||
# ── Option paths are statically visible here ──
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user