234 lines
8.2 KiB
Nix
234 lines
8.2 KiB
Nix
|
{ config
|
||
|
, lib
|
||
|
, options
|
||
|
, pkgs
|
||
|
, ...
|
||
|
}:
|
||
|
let
|
||
|
inherit (lib)
|
||
|
attrValues
|
||
|
concatLines
|
||
|
concatMap
|
||
|
filter
|
||
|
filterAttrsRecursive
|
||
|
flatten
|
||
|
getExe
|
||
|
mdDoc
|
||
|
mkIf
|
||
|
optional
|
||
|
;
|
||
|
|
||
|
cfg = config.services.rosenpass;
|
||
|
opt = options.services.rosenpass;
|
||
|
settingsFormat = pkgs.formats.toml { };
|
||
|
in
|
||
|
{
|
||
|
options.services.rosenpass =
|
||
|
let
|
||
|
inherit (lib)
|
||
|
literalExpression
|
||
|
mdDoc
|
||
|
mkOption
|
||
|
;
|
||
|
inherit (lib.types)
|
||
|
enum
|
||
|
listOf
|
||
|
nullOr
|
||
|
path
|
||
|
str
|
||
|
submodule
|
||
|
;
|
||
|
in
|
||
|
{
|
||
|
enable = lib.mkEnableOption (mdDoc "Rosenpass");
|
||
|
|
||
|
package = lib.mkPackageOption pkgs "rosenpass" { };
|
||
|
|
||
|
defaultDevice = mkOption {
|
||
|
type = nullOr str;
|
||
|
description = mdDoc "Name of the network interface to use for all peers by default.";
|
||
|
example = "wg0";
|
||
|
};
|
||
|
|
||
|
settings = mkOption {
|
||
|
type = submodule {
|
||
|
freeformType = settingsFormat.type;
|
||
|
|
||
|
options = {
|
||
|
public_key = mkOption {
|
||
|
type = path;
|
||
|
description = mdDoc "Path to a file containing the public key of the local Rosenpass peer. Generate this by running {command}`rosenpass gen-keys`.";
|
||
|
};
|
||
|
|
||
|
secret_key = mkOption {
|
||
|
type = path;
|
||
|
description = mdDoc "Path to a file containing the secret key of the local Rosenpass peer. Generate this by running {command}`rosenpass gen-keys`.";
|
||
|
};
|
||
|
|
||
|
listen = mkOption {
|
||
|
type = listOf str;
|
||
|
description = mdDoc "List of local endpoints to listen for connections.";
|
||
|
default = [ ];
|
||
|
example = literalExpression "[ \"0.0.0.0:10000\" ]";
|
||
|
};
|
||
|
|
||
|
verbosity = mkOption {
|
||
|
type = enum [ "Verbose" "Quiet" ];
|
||
|
default = "Quiet";
|
||
|
description = mdDoc "Verbosity of output produced by the service.";
|
||
|
};
|
||
|
|
||
|
peers =
|
||
|
let
|
||
|
peer = submodule {
|
||
|
freeformType = settingsFormat.type;
|
||
|
|
||
|
options = {
|
||
|
public_key = mkOption {
|
||
|
type = path;
|
||
|
description = mdDoc "Path to a file containing the public key of the remote Rosenpass peer.";
|
||
|
};
|
||
|
|
||
|
endpoint = mkOption {
|
||
|
type = nullOr str;
|
||
|
default = null;
|
||
|
description = mdDoc "Endpoint of the remote Rosenpass peer.";
|
||
|
};
|
||
|
|
||
|
device = mkOption {
|
||
|
type = str;
|
||
|
default = cfg.defaultDevice;
|
||
|
defaultText = literalExpression "config.${opt.defaultDevice}";
|
||
|
description = mdDoc "Name of the local WireGuard interface to use for this peer.";
|
||
|
};
|
||
|
|
||
|
peer = mkOption {
|
||
|
type = str;
|
||
|
description = mdDoc "WireGuard public key corresponding to the remote Rosenpass peer.";
|
||
|
};
|
||
|
};
|
||
|
};
|
||
|
in
|
||
|
mkOption {
|
||
|
type = listOf peer;
|
||
|
description = mdDoc "List of peers to exchange keys with.";
|
||
|
default = [ ];
|
||
|
};
|
||
|
};
|
||
|
};
|
||
|
default = { };
|
||
|
description = mdDoc "Configuration for Rosenpass, see <https://rosenpass.eu/> for further information.";
|
||
|
};
|
||
|
};
|
||
|
|
||
|
config = mkIf cfg.enable {
|
||
|
warnings =
|
||
|
let
|
||
|
# NOTE: In the descriptions below, we tried to refer to e.g.
|
||
|
# options.systemd.network.netdevs."<name>".wireguardPeers.*.PublicKey
|
||
|
# directly, but don't know how to traverse "<name>" and * in this path.
|
||
|
extractions = [
|
||
|
{
|
||
|
relevant = config.systemd.network.enable;
|
||
|
root = config.systemd.network.netdevs;
|
||
|
peer = (x: x.wireguardPeers);
|
||
|
key = (x: if x.wireguardPeerConfig ? PublicKey then x.wireguardPeerConfig.PublicKey else null);
|
||
|
description = mdDoc "${options.systemd.network.netdevs}.\"<name>\".wireguardPeers.*.wireguardPeerConfig.PublicKey";
|
||
|
}
|
||
|
{
|
||
|
relevant = config.networking.wireguard.enable;
|
||
|
root = config.networking.wireguard.interfaces;
|
||
|
peer = (x: x.peers);
|
||
|
key = (x: x.publicKey);
|
||
|
description = mdDoc "${options.networking.wireguard.interfaces}.\"<name>\".peers.*.publicKey";
|
||
|
}
|
||
|
rec {
|
||
|
relevant = root != { };
|
||
|
root = config.networking.wg-quick.interfaces;
|
||
|
peer = (x: x.peers);
|
||
|
key = (x: x.publicKey);
|
||
|
description = mdDoc "${options.networking.wg-quick.interfaces}.\"<name>\".peers.*.publicKey";
|
||
|
}
|
||
|
];
|
||
|
relevantExtractions = filter (x: x.relevant) extractions;
|
||
|
extract = { root, peer, key, ... }:
|
||
|
filter (x: x != null) (flatten (concatMap (x: (map key (peer x))) (attrValues root)));
|
||
|
configuredKeys = flatten (map extract relevantExtractions);
|
||
|
itemize = xs: concatLines (map (x: " - ${x}") xs);
|
||
|
descriptions = map (x: "`${x.description}`");
|
||
|
missingKeys = filter (key: !builtins.elem key configuredKeys) (map (x: x.peer) cfg.settings.peers);
|
||
|
unusual = ''
|
||
|
While this may work as expected, e.g. you want to manually configure WireGuard,
|
||
|
such a scenario is unusual. Please double-check your configuration.
|
||
|
'';
|
||
|
in
|
||
|
(optional (relevantExtractions != [ ] && missingKeys != [ ]) ''
|
||
|
You have configured Rosenpass peers with the WireGuard public keys:
|
||
|
${itemize missingKeys}
|
||
|
But there is no corresponding active Wireguard peer configuration in any of:
|
||
|
${itemize (descriptions relevantExtractions)}
|
||
|
${unusual}
|
||
|
'')
|
||
|
++
|
||
|
optional (relevantExtractions == [ ]) ''
|
||
|
You have configured Rosenpass, but you have not configured Wireguard via any of:
|
||
|
${itemize (descriptions extractions)}
|
||
|
${unusual}
|
||
|
'';
|
||
|
|
||
|
environment.systemPackages = [ cfg.package pkgs.wireguard-tools ];
|
||
|
|
||
|
systemd.services.rosenpass =
|
||
|
let
|
||
|
filterNonNull = filterAttrsRecursive (_: v: v != null);
|
||
|
config = settingsFormat.generate "config.toml" (
|
||
|
filterNonNull (cfg.settings
|
||
|
//
|
||
|
(
|
||
|
let
|
||
|
credentialPath = id: "$CREDENTIALS_DIRECTORY/${id}";
|
||
|
# NOTE: We would like to remove all `null` values inside `cfg.settings`
|
||
|
# recursively, since `settingsFormat.generate` cannot handle `null`.
|
||
|
# This would require to traverse both attribute sets and lists recursively.
|
||
|
# `filterAttrsRecursive` only recurses into attribute sets, but not
|
||
|
# into values that might contain other attribute sets (such as lists,
|
||
|
# e.g. `cfg.settings.peers`). Here, we just specialize on `cfg.settings.peers`,
|
||
|
# and this may break unexpectedly whenever a `null` value is contained
|
||
|
# in a list in `cfg.settings`, other than `cfg.settings.peers`.
|
||
|
peersWithoutNulls = map filterNonNull cfg.settings.peers;
|
||
|
in
|
||
|
{
|
||
|
secret_key = credentialPath "pqsk";
|
||
|
public_key = credentialPath "pqpk";
|
||
|
peers = peersWithoutNulls;
|
||
|
}
|
||
|
)
|
||
|
)
|
||
|
);
|
||
|
in
|
||
|
rec {
|
||
|
wantedBy = [ "multi-user.target" ];
|
||
|
after = [ "network-online.target" ];
|
||
|
path = [ cfg.package pkgs.wireguard-tools ];
|
||
|
|
||
|
serviceConfig = {
|
||
|
User = "rosenpass";
|
||
|
Group = "rosenpass";
|
||
|
RuntimeDirectory = "rosenpass";
|
||
|
DynamicUser = true;
|
||
|
AmbientCapabilities = [ "CAP_NET_ADMIN" ];
|
||
|
LoadCredential = [
|
||
|
"pqsk:${cfg.settings.secret_key}"
|
||
|
"pqpk:${cfg.settings.public_key}"
|
||
|
];
|
||
|
};
|
||
|
|
||
|
# See <https://www.freedesktop.org/software/systemd/man/systemd.unit.html#Specifiers>
|
||
|
environment.CONFIG = "%t/${serviceConfig.RuntimeDirectory}/config.toml";
|
||
|
|
||
|
preStart = "${getExe pkgs.envsubst} -i ${config} -o \"$CONFIG\"";
|
||
|
script = "rosenpass exchange-config \"$CONFIG\"";
|
||
|
};
|
||
|
};
|
||
|
}
|