{ 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; }