pi/services/autossh.nix
Katharina Heidenreich ecf10628c3 feat: try rework
2026-04-04 16:34:02 +02:00

145 lines
No EOL
5.2 KiB
Nix

{ config, pkgs, lib, ... }:
let
net = import ../config/network.nix;
rem = import ../intermediate/remote.nix;
autoSshValidation = import ../validation/auto_ssh.nix;
devices = autoSshValidation.getDevices net;
portsByRemote =
if rem ? portsByRemote && builtins.isAttrs rem.portsByRemote then
rem.portsByRemote
else
throw "intermediate/remote.nix must export portsByRemote as an attrset.";
autoSshDevices = autoSshValidation.getAutoSshDevices devices;
getAutoSsh = deviceName: device:
autoSshValidation.getAutoSshConfig deviceName device;
validateForwards = deviceName: forwards:
if !builtins.isList forwards then
throw "portsByRemote.${deviceName} must be a list of forward objects."
else if !lib.all (forward:
builtins.isAttrs forward
&& forward ? remote
&& forward ? localAddress
&& forward ? localPort
&& builtins.isInt forward.remote
&& builtins.isString forward.localAddress
&& builtins.isInt forward.localPort
) forwards then
throw "Each forward in portsByRemote.${deviceName} must be { remote = int; localAddress = string; localPort = int; }."
else
forwards;
getForwards = deviceName:
if portsByRemote ? ${deviceName} then
validateForwards deviceName portsByRemote.${deviceName}
else
throw "Missing mapped forwards for auto_ssh device '${deviceName}' in intermediate/remote.nix portsByRemote.";
enabledAutoSshDevices =
lib.filterAttrs (deviceName: device:
let
_ =
if autoSshValidation.isSafeName deviceName then
null
else
throw "Auto SSH device name '${deviceName}' is not safe for Linux user/systemd names. Use lowercase letters, numbers, '_' or '-' and start with a letter or '_'.";
autoSsh = getAutoSsh deviceName device;
in
if !(autoSsh ? enable) then
throw "Auto SSH device '${deviceName}' is missing required field: auto_ssh.enable."
else if !builtins.isBool autoSsh.enable then
throw "Auto SSH device '${deviceName}' field auto_ssh.enable must be a bool."
else
autoSsh.enable
) autoSshDevices;
getField = deviceName: autoSsh: fieldName: typeCheck: typeName:
if !(autoSsh ? ${fieldName}) then
throw "Auto SSH device '${deviceName}' is missing required field: auto_ssh.${fieldName}."
else if !typeCheck autoSsh.${fieldName} then
throw "Auto SSH device '${deviceName}' field auto_ssh.${fieldName} must be ${typeName}."
else
autoSsh.${fieldName};
mkForwardString = forwards:
builtins.concatStringsSep " " (map (forward:
"-R ${toString forward.remote}:${forward.localAddress}:${toString forward.localPort}"
) forwards);
mkService = deviceName: device:
let
autoSsh = getAutoSsh deviceName device;
sshHost =
if device ? ip && builtins.isString device.ip then
device.ip
else
throw "Auto SSH device '${deviceName}' must define ip as string.";
sshPort = getField deviceName autoSsh "sshPort" builtins.isInt "an int";
sshUser = getField deviceName autoSsh "sshUser" builtins.isString "a string";
sshKeyPath = getField deviceName autoSsh "key" (value: builtins.isString value || builtins.isPath value) "a string or path";
trustedHostsFile = getField deviceName autoSsh "known_hosts" (value: builtins.isString value || builtins.isPath value) "a string or path";
forwards = getForwards deviceName;
_ = if forwards == [] then throw "Auto SSH device '${deviceName}' has no mapped forwards. Add matching endpoints or disable this remote." else null;
forwardString = mkForwardString forwards;
serviceName = "autossh-${deviceName}";
in {
name = serviceName;
value = {
description = "AutoSSH reverse tunnel for ${deviceName}";
after = [ "network.target" "network-online.target" ];
wants = [ "network-online.target" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
Type = "simple";
User = serviceName;
Restart = "always";
RestartSec = 10;
ExecStart = ''
${pkgs.autossh}/bin/autossh \
-N \
-T \
-M 0 \
-o ServerAliveInterval=10 \
-o ExitOnForwardFailure=yes \
-o UserKnownHostsFile=${trustedHostsFile} \
${forwardString} \
-i ${sshKeyPath} \
-p ${toString sshPort} \
${sshUser}@${sshHost}
'';
};
};
};
generatedServices = builtins.listToAttrs (lib.mapAttrsToList mkService enabledAutoSshDevices);
generatedUsers = builtins.listToAttrs (lib.mapAttrsToList (deviceName: _: {
name = "autossh-${deviceName}";
value = {
isSystemUser = true;
group = "autossh-${deviceName}";
extraGroups = [ "keys" ];
description = "AutoSSH user for ${deviceName}";
};
}) enabledAutoSshDevices);
generatedGroups = builtins.listToAttrs (lib.mapAttrsToList (deviceName: _: {
name = "autossh-${deviceName}";
value = {};
}) enabledAutoSshDevices);
in
{
environment.systemPackages = with pkgs; [
autossh
];
users.users = generatedUsers;
users.groups = generatedGroups;
systemd.services = generatedServices;
}