145 lines
No EOL
5.2 KiB
Nix
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;
|
|
} |