Some checks are pending
linux-lab / core-fast (push) Waiting to run
linux-lab / core-nat (push) Waiting to run
linux-lab / core-netem (push) Waiting to run
linux-lab / extended-auth-url (push) Waiting to run
linux-lab / extended-nat-churn (push) Waiting to run
linux-lab / extended-relay-switch (push) Waiting to run
linux-lab / extended-soak (push) Waiting to run
linux-lab / extended-standalone (push) Waiting to run
470 lines
15 KiB
Nix
470 lines
15 KiB
Nix
{ defaultPackage }:
|
|
{ config, lib, pkgs, ... }:
|
|
let
|
|
cfg = config.services.lightscale-client;
|
|
|
|
stateProfileDir = "${cfg.stateDir}/${cfg.profile}";
|
|
|
|
baseArgs =
|
|
[ "--profile" cfg.profile ]
|
|
++ lib.optionals (cfg.configFile != null) [ "--config" cfg.configFile ]
|
|
++ [ "--state-dir" stateProfileDir ]
|
|
++ lib.optionals (cfg.controlUrls != [ ]) [ "--control-url" (lib.concatStringsSep "," cfg.controlUrls) ]
|
|
++ lib.optionals (cfg.tlsPin != null) [ "--tls-pin" cfg.tlsPin ]
|
|
++ lib.optionals (cfg.adminToken != null) [ "--admin-token" cfg.adminToken ];
|
|
|
|
agentArgs =
|
|
[ "agent" "--listen-port" (toString cfg.listenPort) ]
|
|
++ lib.optionals (cfg.interface != null) [ "--interface" cfg.interface ]
|
|
++ lib.optionals cfg.applyRoutes [ "--apply-routes" ]
|
|
++ lib.optionals cfg.acceptExitNode [ "--accept-exit-node" ]
|
|
++ lib.optionals (cfg.exitNodeId != null) [ "--exit-node-id" cfg.exitNodeId ]
|
|
++ lib.optionals (cfg.exitNodeName != null) [ "--exit-node-name" cfg.exitNodeName ]
|
|
++ lib.optionals (cfg.exitNodeTag != null) [ "--exit-node-tag" cfg.exitNodeTag ]
|
|
++ [ "--exit-node-policy" cfg.exitNodePolicy ]
|
|
++ lib.optionals (cfg.exitNodeMetricBase != null) [ "--exit-node-metric-base" (toString cfg.exitNodeMetricBase) ]
|
|
++ lib.optionals (cfg.exitNodeUidRange != null) [ "--exit-node-uid-range" cfg.exitNodeUidRange ]
|
|
++ lib.optionals cfg.allowRouteConflicts [ "--allow-route-conflicts" ]
|
|
++ lib.optionals (cfg.routeTable != null) [ "--route-table" (toString cfg.routeTable) ]
|
|
++ lib.concatMap (endpoint: [ "--endpoint" endpoint ]) cfg.endpoints
|
|
++ lib.concatMap (prefix: [ "--advertise-route" prefix ]) cfg.advertiseRoutes
|
|
++ lib.concatMap (mapping: [ "--advertise-map" mapping ]) cfg.advertiseMaps
|
|
++ lib.optionals cfg.advertiseExitNode [ "--advertise-exit-node" ]
|
|
++ [ "--heartbeat-interval" (toString cfg.heartbeatInterval) ]
|
|
++ [ "--longpoll-timeout" (toString cfg.longpollTimeout) ]
|
|
++ [ "--backend" cfg.backend ]
|
|
++ lib.optionals cfg.stun [ "--stun" ]
|
|
++ lib.optionals (cfg.stunServers != [ ]) [ "--stun-server" (lib.concatStringsSep "," cfg.stunServers) ]
|
|
++ lib.optionals (cfg.stunPort != null) [ "--stun-port" (toString cfg.stunPort) ]
|
|
++ [ "--stun-timeout" (toString cfg.stunTimeout) ]
|
|
++ lib.optionals cfg.probePeers [ "--probe-peers" ]
|
|
++ [ "--probe-timeout" (toString cfg.probeTimeout) ]
|
|
++ lib.optionals cfg.streamRelay [ "--stream-relay" ]
|
|
++ lib.optionals (cfg.streamRelayServers != [ ]) [ "--stream-relay-server" (lib.concatStringsSep "," cfg.streamRelayServers) ]
|
|
++ [ "--endpoint-stale-after" (toString cfg.endpointStaleAfter) ]
|
|
++ [ "--endpoint-max-rotations" (toString cfg.endpointMaxRotations) ]
|
|
++ [ "--relay-reprobe-after" (toString cfg.relayReprobeAfter) ]
|
|
++ lib.optionals (cfg.dnsHostsPath != null) [ "--dns-hosts-path" cfg.dnsHostsPath ]
|
|
++ lib.optionals cfg.dnsServe [ "--dns-serve" ]
|
|
++ lib.optionals (cfg.dnsListen != null) [ "--dns-listen" cfg.dnsListen ]
|
|
++ lib.optionals cfg.dnsApplyResolver [ "--dns-apply-resolver" ]
|
|
++ lib.optionals cfg.l2Relay [ "--l2-relay" ]
|
|
++ cfg.extraArgs;
|
|
|
|
args = baseArgs ++ agentArgs;
|
|
startCmd = "${lib.getExe' cfg.package "lightscale-client"} ${lib.escapeShellArgs args}";
|
|
|
|
registerArgs =
|
|
baseArgs
|
|
++ [ "register" ]
|
|
++ lib.optionals (cfg.registerNodeName != null) [ "--node-name" cfg.registerNodeName ]
|
|
++ cfg.registerExtraArgs;
|
|
|
|
registerScript = pkgs.writeShellScript "lightscale-client-register-${cfg.profile}" ''
|
|
set -euo pipefail
|
|
|
|
state_file=${lib.escapeShellArg "${stateProfileDir}/state.json"}
|
|
if [[ -s "$state_file" ]]; then
|
|
exit 0
|
|
fi
|
|
|
|
token_file=${lib.escapeShellArg cfg.enrollmentTokenFile}
|
|
if [[ ! -r "$token_file" ]]; then
|
|
echo "enrollment token file not readable: $token_file" >&2
|
|
exit 1
|
|
fi
|
|
|
|
token="$(tr -d '\r\n' < "$token_file")"
|
|
if [[ -z "$token" ]]; then
|
|
echo "enrollment token is empty" >&2
|
|
exit 1
|
|
fi
|
|
|
|
exec ${lib.getExe' cfg.package "lightscale-client"} ${lib.escapeShellArgs registerArgs} -- "$token"
|
|
'';
|
|
|
|
defaultUdpPorts = lib.unique ([ cfg.listenPort ] ++ cfg.firewallUDPPorts);
|
|
in
|
|
{
|
|
options.services.lightscale-client = {
|
|
enable = lib.mkEnableOption "lightscale client agent";
|
|
|
|
package = lib.mkOption {
|
|
type = lib.types.package;
|
|
default = defaultPackage;
|
|
description = "lightscale-client package.";
|
|
};
|
|
|
|
profile = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = "default";
|
|
description = "Profile name passed to lightscale-client.";
|
|
};
|
|
|
|
stateDir = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = "/var/lib/lightscale-client";
|
|
description = "Base state directory; per-profile state is stored under stateDir/profile.";
|
|
};
|
|
|
|
configFile = lib.mkOption {
|
|
type = lib.types.nullOr lib.types.str;
|
|
default = null;
|
|
description = "Optional config file path for profiles and TLS pin.";
|
|
};
|
|
|
|
controlUrls = lib.mkOption {
|
|
type = lib.types.listOf lib.types.str;
|
|
default = [ ];
|
|
description = "Control plane URLs passed via --control-url (comma-separated failover supported).";
|
|
};
|
|
|
|
tlsPin = lib.mkOption {
|
|
type = lib.types.nullOr lib.types.str;
|
|
default = null;
|
|
description = "Optional SHA-256 TLS pin (same format as `lightscale-client pin`).";
|
|
};
|
|
|
|
adminToken = lib.mkOption {
|
|
type = lib.types.nullOr lib.types.str;
|
|
default = null;
|
|
description = "Optional admin token flag. Prefer environmentFiles for secrets.";
|
|
};
|
|
|
|
autoRegister = lib.mkOption {
|
|
type = lib.types.bool;
|
|
default = false;
|
|
description = "Run a one-shot registration service when state file is missing.";
|
|
};
|
|
|
|
enrollmentTokenFile = lib.mkOption {
|
|
type = lib.types.nullOr lib.types.str;
|
|
default = null;
|
|
description = "Path to enrollment token file for autoRegister.";
|
|
};
|
|
|
|
registerNodeName = lib.mkOption {
|
|
type = lib.types.nullOr lib.types.str;
|
|
default = null;
|
|
description = "Optional node name used by autoRegister.";
|
|
};
|
|
|
|
registerExtraArgs = lib.mkOption {
|
|
type = lib.types.listOf lib.types.str;
|
|
default = [ ];
|
|
description = "Extra args appended to autoRegister `register` command.";
|
|
};
|
|
|
|
environmentFiles = lib.mkOption {
|
|
type = lib.types.listOf lib.types.str;
|
|
default = [ ];
|
|
description = "systemd EnvironmentFile entries (for LIGHTSCALE_ADMIN_TOKEN, etc.).";
|
|
};
|
|
|
|
environment = lib.mkOption {
|
|
type = lib.types.attrsOf lib.types.str;
|
|
default = { };
|
|
description = "Additional environment variables for the client service.";
|
|
};
|
|
|
|
interface = lib.mkOption {
|
|
type = lib.types.nullOr lib.types.str;
|
|
default = null;
|
|
description = "WireGuard interface name. Default is derived from profile.";
|
|
};
|
|
|
|
listenPort = lib.mkOption {
|
|
type = lib.types.port;
|
|
default = 51820;
|
|
description = "WireGuard listen port for the agent.";
|
|
};
|
|
|
|
backend = lib.mkOption {
|
|
type = lib.types.enum [ "kernel" "boringtun" ];
|
|
default = "kernel";
|
|
description = "WireGuard backend used by the agent.";
|
|
};
|
|
|
|
endpoints = lib.mkOption {
|
|
type = lib.types.listOf lib.types.str;
|
|
default = [ ];
|
|
description = "Advertised endpoints (HOST:PORT), passed as repeated --endpoint flags.";
|
|
};
|
|
|
|
applyRoutes = lib.mkOption {
|
|
type = lib.types.bool;
|
|
default = true;
|
|
description = "Apply advertised subnet/exit routes on netmap updates.";
|
|
};
|
|
|
|
acceptExitNode = lib.mkOption {
|
|
type = lib.types.bool;
|
|
default = false;
|
|
description = "Accept exit routes from selected peers.";
|
|
};
|
|
|
|
exitNodeId = lib.mkOption {
|
|
type = lib.types.nullOr lib.types.str;
|
|
default = null;
|
|
description = "Specific exit node ID to use.";
|
|
};
|
|
|
|
exitNodeName = lib.mkOption {
|
|
type = lib.types.nullOr lib.types.str;
|
|
default = null;
|
|
description = "Specific exit node name to use.";
|
|
};
|
|
|
|
exitNodePolicy = lib.mkOption {
|
|
type = lib.types.enum [ "first" "latest" "multi" ];
|
|
default = "first";
|
|
description = "Policy for selecting exit nodes.";
|
|
};
|
|
|
|
exitNodeTag = lib.mkOption {
|
|
type = lib.types.nullOr lib.types.str;
|
|
default = null;
|
|
description = "Only consider exit nodes with this tag.";
|
|
};
|
|
|
|
exitNodeMetricBase = lib.mkOption {
|
|
type = lib.types.nullOr lib.types.ints.unsigned;
|
|
default = null;
|
|
description = "Base route metric used for selected exit routes.";
|
|
};
|
|
|
|
exitNodeUidRange = lib.mkOption {
|
|
type = lib.types.nullOr lib.types.str;
|
|
default = null;
|
|
description = "UID or UID range for per-user exit routing (e.g. 1000-1999).";
|
|
};
|
|
|
|
allowRouteConflicts = lib.mkOption {
|
|
type = lib.types.bool;
|
|
default = false;
|
|
description = "Force route installation even when conflicts are detected.";
|
|
};
|
|
|
|
routeTable = lib.mkOption {
|
|
type = lib.types.nullOr lib.types.ints.unsigned;
|
|
default = null;
|
|
description = "Optional policy routing table ID.";
|
|
};
|
|
|
|
advertiseRoutes = lib.mkOption {
|
|
type = lib.types.listOf lib.types.str;
|
|
default = [ ];
|
|
description = "Advertised subnet routes (CIDR).";
|
|
};
|
|
|
|
advertiseMaps = lib.mkOption {
|
|
type = lib.types.listOf lib.types.str;
|
|
default = [ ];
|
|
description = "Route map entries in REAL=MAPPED form.";
|
|
};
|
|
|
|
advertiseExitNode = lib.mkOption {
|
|
type = lib.types.bool;
|
|
default = false;
|
|
description = "Advertise this node as an exit node.";
|
|
};
|
|
|
|
heartbeatInterval = lib.mkOption {
|
|
type = lib.types.ints.positive;
|
|
default = 30;
|
|
description = "Heartbeat interval in seconds.";
|
|
};
|
|
|
|
longpollTimeout = lib.mkOption {
|
|
type = lib.types.ints.positive;
|
|
default = 30;
|
|
description = "Netmap long-poll timeout in seconds.";
|
|
};
|
|
|
|
stun = lib.mkOption {
|
|
type = lib.types.bool;
|
|
default = false;
|
|
description = "Enable STUN endpoint discovery in the agent.";
|
|
};
|
|
|
|
stunServers = lib.mkOption {
|
|
type = lib.types.listOf lib.types.str;
|
|
default = [ ];
|
|
description = "Explicit STUN servers (HOST:PORT).";
|
|
};
|
|
|
|
stunPort = lib.mkOption {
|
|
type = lib.types.nullOr lib.types.port;
|
|
default = null;
|
|
description = "Optional local UDP port for STUN probe socket.";
|
|
};
|
|
|
|
stunTimeout = lib.mkOption {
|
|
type = lib.types.ints.positive;
|
|
default = 3;
|
|
description = "STUN timeout in seconds.";
|
|
};
|
|
|
|
probePeers = lib.mkOption {
|
|
type = lib.types.bool;
|
|
default = false;
|
|
description = "Send UDP probes to peers when netmap changes.";
|
|
};
|
|
|
|
probeTimeout = lib.mkOption {
|
|
type = lib.types.ints.positive;
|
|
default = 1;
|
|
description = "UDP probe timeout in seconds.";
|
|
};
|
|
|
|
streamRelay = lib.mkOption {
|
|
type = lib.types.bool;
|
|
default = false;
|
|
description = "Enable stream-relay endpoint fallback support.";
|
|
};
|
|
|
|
streamRelayServers = lib.mkOption {
|
|
type = lib.types.listOf lib.types.str;
|
|
default = [ ];
|
|
description = "Explicit stream relay servers (HOST:PORT).";
|
|
};
|
|
|
|
endpointStaleAfter = lib.mkOption {
|
|
type = lib.types.ints.positive;
|
|
default = 15;
|
|
description = "Seconds before endpoint is considered stale.";
|
|
};
|
|
|
|
endpointMaxRotations = lib.mkOption {
|
|
type = lib.types.ints.positive;
|
|
default = 2;
|
|
description = "Maximum endpoint rotations before relay fallback.";
|
|
};
|
|
|
|
relayReprobeAfter = lib.mkOption {
|
|
type = lib.types.ints.positive;
|
|
default = 60;
|
|
description = "Seconds between direct re-probes while relay is active.";
|
|
};
|
|
|
|
dnsHostsPath = lib.mkOption {
|
|
type = lib.types.nullOr lib.types.str;
|
|
default = null;
|
|
description = "Optional hosts file path updated from netmap.";
|
|
};
|
|
|
|
dnsServe = lib.mkOption {
|
|
type = lib.types.bool;
|
|
default = false;
|
|
description = "Serve MagicDNS-like answers for the overlay domain.";
|
|
};
|
|
|
|
dnsListen = lib.mkOption {
|
|
type = lib.types.nullOr lib.types.str;
|
|
default = null;
|
|
description = "DNS listen address (defaults to 127.0.0.1:53 when dnsServe=true).";
|
|
};
|
|
|
|
dnsApplyResolver = lib.mkOption {
|
|
type = lib.types.bool;
|
|
default = false;
|
|
description = "Apply per-domain resolver routing via resolvectl.";
|
|
};
|
|
|
|
l2Relay = lib.mkOption {
|
|
type = lib.types.bool;
|
|
default = false;
|
|
description = "Enable experimental L2 relay behavior.";
|
|
};
|
|
|
|
extraArgs = lib.mkOption {
|
|
type = lib.types.listOf lib.types.str;
|
|
default = [ ];
|
|
description = "Additional CLI arguments appended to the agent command.";
|
|
};
|
|
|
|
openFirewall = lib.mkOption {
|
|
type = lib.types.bool;
|
|
default = false;
|
|
description = "Open firewall ports listed in firewallTCPPorts and listen UDP port(s).";
|
|
};
|
|
|
|
firewallTCPPorts = lib.mkOption {
|
|
type = lib.types.listOf lib.types.port;
|
|
default = [ ];
|
|
description = "Extra TCP ports to open when openFirewall=true.";
|
|
};
|
|
|
|
firewallUDPPorts = lib.mkOption {
|
|
type = lib.types.listOf lib.types.port;
|
|
default = [ ];
|
|
description = "Extra UDP ports to open when openFirewall=true (listenPort is always included).";
|
|
};
|
|
};
|
|
|
|
config = lib.mkIf cfg.enable {
|
|
assertions = [
|
|
{
|
|
assertion = (!cfg.autoRegister) || (cfg.enrollmentTokenFile != null);
|
|
message = "services.lightscale-client: autoRegister=true requires enrollmentTokenFile.";
|
|
}
|
|
];
|
|
|
|
systemd.services.lightscale-client-register = lib.mkIf cfg.autoRegister {
|
|
description = "lightscale client one-shot registration (${cfg.profile})";
|
|
wantedBy = [ "multi-user.target" ];
|
|
before = [ "lightscale-client.service" ];
|
|
after = [ "network-online.target" ];
|
|
wants = [ "network-online.target" ];
|
|
path = [ pkgs.coreutils ];
|
|
unitConfig = {
|
|
ConditionPathExists = "!${stateProfileDir}/state.json";
|
|
};
|
|
serviceConfig = {
|
|
Type = "oneshot";
|
|
ExecStart = registerScript;
|
|
EnvironmentFile = cfg.environmentFiles;
|
|
};
|
|
};
|
|
|
|
systemd.services.lightscale-client = {
|
|
description = "lightscale client agent";
|
|
wantedBy = [ "multi-user.target" ];
|
|
after = [ "network-online.target" ] ++ lib.optionals cfg.autoRegister [ "lightscale-client-register.service" ];
|
|
wants = [ "network-online.target" ] ++ lib.optionals cfg.autoRegister [ "lightscale-client-register.service" ];
|
|
unitConfig = {
|
|
ConditionPathExists = "${stateProfileDir}/state.json";
|
|
};
|
|
path = [
|
|
pkgs.iproute2
|
|
pkgs.nftables
|
|
pkgs.systemd
|
|
];
|
|
environment = cfg.environment;
|
|
preStart = ''
|
|
install -d -m 0750 ${lib.escapeShellArg cfg.stateDir}
|
|
install -d -m 0750 ${lib.escapeShellArg stateProfileDir}
|
|
'';
|
|
serviceConfig = {
|
|
Type = "simple";
|
|
ExecStart = startCmd;
|
|
Restart = "on-failure";
|
|
RestartSec = 2;
|
|
EnvironmentFile = cfg.environmentFiles;
|
|
CapabilityBoundingSet = [
|
|
"CAP_NET_ADMIN"
|
|
"CAP_NET_RAW"
|
|
"CAP_NET_BIND_SERVICE"
|
|
];
|
|
AmbientCapabilities = [
|
|
"CAP_NET_ADMIN"
|
|
"CAP_NET_RAW"
|
|
"CAP_NET_BIND_SERVICE"
|
|
];
|
|
};
|
|
};
|
|
|
|
networking.firewall.allowedTCPPorts = lib.mkIf cfg.openFirewall cfg.firewallTCPPorts;
|
|
networking.firewall.allowedUDPPorts = lib.mkIf cfg.openFirewall defaultUdpPorts;
|
|
};
|
|
}
|