diff --git a/nixos/doc/manual/release-notes/rl-2311.section.md b/nixos/doc/manual/release-notes/rl-2311.section.md
index be4ce3c27d8b..bc2a15124275 100644
--- a/nixos/doc/manual/release-notes/rl-2311.section.md
+++ b/nixos/doc/manual/release-notes/rl-2311.section.md
@@ -121,6 +121,8 @@
- [Soft Serve](https://github.com/charmbracelet/soft-serve), a tasty, self-hostable Git server for the command line. Available as [services.soft-serve](#opt-services.soft-serve.enable).
+- [Rosenpass](https://rosenpass.eu/), a service for post-quantum-secure VPNs with WireGuard. Available as [services.rosenpass](#opt-services.rosenpass.enable).
+
## Backward Incompatibilities {#sec-release-23.11-incompatibilities}
- `network-online.target` has been fixed to no longer time out for systems with `networking.useDHCP = true` and `networking.useNetworkd = true`.
diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix
index 47b262bf4d98..81b7b981a446 100644
--- a/nixos/modules/module-list.nix
+++ b/nixos/modules/module-list.nix
@@ -1047,6 +1047,7 @@
./services/networking/redsocks.nix
./services/networking/resilio.nix
./services/networking/robustirc-bridge.nix
+ ./services/networking/rosenpass.nix
./services/networking/routedns.nix
./services/networking/rpcbind.nix
./services/networking/rxe.nix
diff --git a/nixos/modules/services/networking/rosenpass.nix b/nixos/modules/services/networking/rosenpass.nix
new file mode 100644
index 000000000000..d2a264b83d67
--- /dev/null
+++ b/nixos/modules/services/networking/rosenpass.nix
@@ -0,0 +1,233 @@
+{ 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 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."".wireguardPeers.*.PublicKey
+ # directly, but don't know how to traverse "" 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}.\"\".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}.\"\".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}.\"\".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
+ environment.CONFIG = "%t/${serviceConfig.RuntimeDirectory}/config.toml";
+
+ preStart = "${getExe pkgs.envsubst} -i ${config} -o \"$CONFIG\"";
+ script = "rosenpass exchange-config \"$CONFIG\"";
+ };
+ };
+}
diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix
index 42d620b512c4..d9d58bbd66a8 100644
--- a/nixos/tests/all-tests.nix
+++ b/nixos/tests/all-tests.nix
@@ -703,6 +703,7 @@ in {
rkvm = handleTest ./rkvm {};
robustirc-bridge = handleTest ./robustirc-bridge.nix {};
roundcube = handleTest ./roundcube.nix {};
+ rosenpass = handleTest ./rosenpass.nix {};
rshim = handleTest ./rshim.nix {};
rspamd = handleTest ./rspamd.nix {};
rss2email = handleTest ./rss2email.nix {};
diff --git a/nixos/tests/rosenpass.nix b/nixos/tests/rosenpass.nix
new file mode 100644
index 000000000000..ec4046c8c035
--- /dev/null
+++ b/nixos/tests/rosenpass.nix
@@ -0,0 +1,217 @@
+import ./make-test-python.nix ({ pkgs, ... }:
+let
+ deviceName = "rp0";
+
+ server = {
+ ip = "fe80::1";
+ wg = {
+ public = "mQufmDFeQQuU/fIaB2hHgluhjjm1ypK4hJr1cW3WqAw=";
+ secret = "4N5Y1dldqrpsbaEiY8O0XBUGUFf8vkvtBtm8AoOX7Eo=";
+ listen = 10000;
+ };
+ };
+ client = {
+ ip = "fe80::2";
+ wg = {
+ public = "Mb3GOlT7oS+F3JntVKiaD7SpHxLxNdtEmWz/9FMnRFU=";
+ secret = "uC5dfGMv7Oxf5UDfdPkj6rZiRZT2dRWp5x8IQxrNcUE=";
+ };
+ };
+in
+{
+ name = "rosenpass";
+
+ nodes =
+ let
+ shared = peer: { config, modulesPath, ... }: {
+ imports = [ "${modulesPath}/services/networking/rosenpass.nix" ];
+
+ boot.kernelModules = [ "wireguard" ];
+
+ services.rosenpass = {
+ enable = true;
+ defaultDevice = deviceName;
+ settings = {
+ verbosity = "Verbose";
+ public_key = "/etc/rosenpass/pqpk";
+ secret_key = "/etc/rosenpass/pqsk";
+ };
+ };
+
+ networking.firewall.allowedUDPPorts = [ 9999 ];
+
+ systemd.network = {
+ enable = true;
+ networks."rosenpass" = {
+ matchConfig.Name = deviceName;
+ networkConfig.IPForward = true;
+ address = [ "${peer.ip}/64" ];
+ };
+
+ netdevs."10-rp0" = {
+ netdevConfig = {
+ Kind = "wireguard";
+ Name = deviceName;
+ };
+ wireguardConfig.PrivateKeyFile = "/etc/wireguard/wgsk";
+ };
+ };
+
+ environment.etc."wireguard/wgsk" = {
+ text = peer.wg.secret;
+ user = "systemd-network";
+ group = "systemd-network";
+ };
+ };
+ in
+ {
+ server = {
+ imports = [ (shared server) ];
+
+ networking.firewall.allowedUDPPorts = [ server.wg.listen ];
+
+ systemd.network.netdevs."10-${deviceName}" = {
+ wireguardConfig.ListenPort = server.wg.listen;
+ wireguardPeers = [
+ {
+ wireguardPeerConfig = {
+ AllowedIPs = [ "::/0" ];
+ PublicKey = client.wg.public;
+ };
+ }
+ ];
+ };
+
+ services.rosenpass.settings = {
+ listen = [ "0.0.0.0:9999" ];
+ peers = [
+ {
+ public_key = "/etc/rosenpass/peers/client/pqpk";
+ peer = client.wg.public;
+ }
+ ];
+ };
+ };
+ client = {
+ imports = [ (shared client) ];
+
+ systemd.network.netdevs."10-${deviceName}".wireguardPeers = [
+ {
+ wireguardPeerConfig = {
+ AllowedIPs = [ "::/0" ];
+ PublicKey = server.wg.public;
+ Endpoint = "server:${builtins.toString server.wg.listen}";
+ };
+ }
+ ];
+
+ services.rosenpass.settings.peers = [
+ {
+ public_key = "/etc/rosenpass/peers/server/pqpk";
+ endpoint = "server:9999";
+ peer = server.wg.public;
+ }
+ ];
+ };
+ };
+
+ testScript = { ... }: ''
+ from os import system
+
+ # Full path to rosenpass in the store, to avoid fiddling with `$PATH`.
+ rosenpass = "${pkgs.rosenpass}/bin/rosenpass"
+
+ # Path in `/etc` where keys will be placed.
+ etc = "/etc/rosenpass"
+
+ start_all()
+
+ for machine in [server, client]:
+ machine.wait_for_unit("multi-user.target")
+
+ # Gently stop Rosenpass to avoid crashes during key generation/distribution.
+ for machine in [server, client]:
+ machine.execute("systemctl stop rosenpass.service")
+
+ for (name, machine, remote) in [("server", server, client), ("client", client, server)]:
+ pk, sk = f"{name}.pqpk", f"{name}.pqsk"
+ system(f"{rosenpass} gen-keys --force --secret-key {sk} --public-key {pk}")
+ machine.copy_from_host(sk, f"{etc}/pqsk")
+ machine.copy_from_host(pk, f"{etc}/pqpk")
+ remote.copy_from_host(pk, f"{etc}/peers/{name}/pqpk")
+
+ for machine in [server, client]:
+ machine.execute("systemctl start rosenpass.service")
+
+ for machine in [server, client]:
+ machine.wait_for_unit("rosenpass.service")
+
+ with subtest("ping"):
+ client.succeed("ping -c 2 -i 0.5 ${server.ip}%${deviceName}")
+
+ with subtest("preshared-keys"):
+ # Rosenpass works by setting the WireGuard preshared key at regular intervals.
+ # Thus, if it is not active, then no key will be set, and the output of `wg show` will contain "none".
+ # Otherwise, if it is active, then the key will be set and "none" will not be found in the output of `wg show`.
+ for machine in [server, client]:
+ machine.wait_until_succeeds("wg show all preshared-keys | grep --invert-match none", timeout=5)
+ '';
+
+ # NOTE: Below configuration is for "interactive" (=developing/debugging) only.
+ interactive.nodes =
+ let
+ inherit (import ./ssh-keys.nix pkgs) snakeOilPublicKey snakeOilPrivateKey;
+
+ sshAndKeyGeneration = {
+ services.openssh.enable = true;
+ users.users.root.openssh.authorizedKeys.keys = [ snakeOilPublicKey ];
+ environment.systemPackages = [
+ (pkgs.writeShellApplication {
+ name = "gen-keys";
+ runtimeInputs = [ pkgs.rosenpass ];
+ text = ''
+ HOST="$(hostname)"
+ if [ "$HOST" == "server" ]
+ then
+ PEER="client"
+ else
+ PEER="server"
+ fi
+
+ # Generate keypair.
+ mkdir -vp /etc/rosenpass/peers/$PEER
+ rosenpass gen-keys --force --secret-key /etc/rosenpass/pqsk --public-key /etc/rosenpass/pqpk
+
+ # Set up SSH key.
+ mkdir -p /root/.ssh
+ cp ${snakeOilPrivateKey} /root/.ssh/id_ecdsa
+ chmod 0400 /root/.ssh/id_ecdsa
+
+ # Copy public key to other peer.
+ # shellcheck disable=SC2029
+ ssh -o StrictHostKeyChecking=no $PEER "mkdir -pv /etc/rosenpass/peers/$HOST"
+ scp /etc/rosenpass/pqpk "$PEER:/etc/rosenpass/peers/$HOST/pqpk"
+ '';
+ })
+ ];
+ };
+
+ # Use kmscon
+ # to provide a slightly nicer console, and while we're at it,
+ # also use a nice font.
+ # With kmscon, we can for example zoom in/out using [Ctrl] + [+]
+ # and [Ctrl] + [-]
+ niceConsoleAndAutologin.services.kmscon = {
+ enable = true;
+ autologinUser = "root";
+ fonts = [{
+ name = "Fira Code";
+ package = pkgs.fira-code;
+ }];
+ };
+ in
+ {
+ server = sshAndKeyGeneration // niceConsoleAndAutologin;
+ client = sshAndKeyGeneration // niceConsoleAndAutologin;
+ };
+})
diff --git a/pkgs/tools/misc/envsubst/default.nix b/pkgs/tools/misc/envsubst/default.nix
index b3a1be04d929..8cd59df31b1e 100644
--- a/pkgs/tools/misc/envsubst/default.nix
+++ b/pkgs/tools/misc/envsubst/default.nix
@@ -22,5 +22,6 @@ buildGoModule rec {
homepage = "https://github.com/a8m/envsubst";
license = licenses.mit;
maintainers = with maintainers; [ nicknovitski ];
+ mainProgram = "envsubst";
};
}
diff --git a/pkgs/tools/networking/rosenpass/default.nix b/pkgs/tools/networking/rosenpass/default.nix
index 07669cfeb01e..9467904fe698 100644
--- a/pkgs/tools/networking/rosenpass/default.nix
+++ b/pkgs/tools/networking/rosenpass/default.nix
@@ -1,85 +1,53 @@
{ lib
-, targetPlatform
, fetchFromGitHub
+, nixosTests
, rustPlatform
+, targetPlatform
+, installShellFiles
, cmake
-, makeWrapper
-, pkg-config
-, removeReferencesTo
-, coreutils
-, findutils
-, gawk
-, wireguard-tools
-, bash
, libsodium
+, pkg-config
}:
-
-let
- rpBinPath = lib.makeBinPath [
- coreutils
- findutils
- gawk
- wireguard-tools
- ];
-in
rustPlatform.buildRustPackage rec {
pname = "rosenpass";
- version = "0.2.0";
+ version = "unstable-2023-09-28";
+
src = fetchFromGitHub {
owner = pname;
repo = pname;
- rev = "v${version}";
- sha256 = "sha256-r7/3C5DzXP+9w4rp9XwbP+/NK1axIP6s3Iiio1xRMbk=";
+ rev = "b15f17133f8b5c3c5175b4cfd4fc10039a4e203f";
+ hash = "sha256-UXAkmt4VY0irLK2k4t6SW+SEodFE3CbX5cFbsPG0ZCo=";
};
- cargoHash = "sha256-g2w3lZXQ3Kg3ydKdFs8P2lOPfIkfTbAF0MhxsJoX/E4=";
+ cargoHash = "sha256-N1DQHkgKgkDQ6DbgQJlpZkZ7AMTqX3P8R/cWr14jK2I=";
nativeBuildInputs = [
cmake # for oqs build in the oqs-sys crate
- makeWrapper # for the rp shellscript
- pkg-config # let libsodium-sys-stable find libsodium
- removeReferencesTo
+ pkg-config
rustPlatform.bindgenHook # for C-bindings in the crypto libs
+ installShellFiles
];
- buildInputs = [
- bash # for patchShebangs to find it
- libsodium
- ];
-
- # otherwise pkg-config tries to link non-existent dynamic libs during the build of liboqs
- PKG_CONFIG_ALL_STATIC = true;
-
- # liboqs requires quite a lot of stack memory, thus we adjust the default stack size picked for
- # new threads (which is used by `cargo test`) to be _big enough_
- RUST_MIN_STACK = 8 * 1024 * 1024; # 8 MiB
+ buildInputs = [ libsodium ];
# nix defaults to building for aarch64 _without_ the armv8-a
# crypto extensions, but liboqs depends on these
- preBuild = lib.optionalString targetPlatform.isAarch
- ''NIX_CFLAGS_COMPILE="$NIX_CFLAGS_COMPILE -march=armv8-a+crypto"'';
-
- preInstall = ''
- install -D rp $out/bin/rp
- wrapProgram $out/bin/rp --prefix PATH : "${ rpBinPath }"
- for file in doc/*.1
- do
- install -D $file $out/share/man/man1/''${file##*/}
- done
+ preBuild = lib.optionalString targetPlatform.isAarch64 ''
+ NIX_CFLAGS_COMPILE="$NIX_CFLAGS_COMPILE -march=armv8-a+crypto"
'';
- # nix propagates the *.dev outputs of buildInputs for static builds, but that is non-sense for an
- # executables only package
- postFixup = ''
- find -type f -exec remove-references-to -t ${bash.dev} \
- -t ${libsodium.dev} {} \;
+ postInstall = ''
+ installManPage doc/rosenpass.1
'';
+ passthru.tests.rosenpass = nixosTests.rosenpass;
+
meta = with lib; {
description = "Build post-quantum-secure VPNs with WireGuard!";
homepage = "https://rosenpass.eu/";
license = with licenses; [ mit /* or */ asl20 ];
maintainers = with maintainers; [ wucke13 ];
- platforms = platforms.all;
+ platforms = [ "aarch64-darwin" "aarch64-linux" "x86_64-darwin" "x86_64-linux" ];
+ mainProgram = "rosenpass";
};
}
diff --git a/pkgs/tools/networking/rosenpass/tools.nix b/pkgs/tools/networking/rosenpass/tools.nix
new file mode 100644
index 000000000000..fb59436b3810
--- /dev/null
+++ b/pkgs/tools/networking/rosenpass/tools.nix
@@ -0,0 +1,30 @@
+{ lib
+, stdenv
+, makeWrapper
+, installShellFiles
+, coreutils
+, findutils
+, gawk
+, rosenpass
+, wireguard-tools
+}:
+stdenv.mkDerivation {
+ inherit (rosenpass) version src;
+ pname = "rosenpass-tools";
+
+ nativeBuildInputs = [ makeWrapper installShellFiles ];
+
+ postInstall = ''
+ install -D $src/rp $out/bin/rp
+ installManPage $src/doc/rp.1
+ wrapProgram $out/bin/rp \
+ --prefix PATH : ${lib.makeBinPath [
+ coreutils findutils gawk rosenpass wireguard-tools
+ ]}
+ '';
+
+ meta = rosenpass.meta // {
+ description = "This package contains the Rosenpass tool `rp`, which is a script that wraps the `rosenpass` binary.";
+ mainProgram = "rp";
+ };
+}
diff --git a/pkgs/top-level/all-packages.nix b/pkgs/top-level/all-packages.nix
index c3ad477f7427..bdcbe115f01f 100644
--- a/pkgs/top-level/all-packages.nix
+++ b/pkgs/top-level/all-packages.nix
@@ -12827,6 +12827,8 @@ with pkgs;
rosenpass = callPackage ../tools/networking/rosenpass { };
+ rosenpass-tools = callPackage ../tools/networking/rosenpass/tools.nix { };
+
rot8 = callPackage ../tools/misc/rot8 { };
rowhammer-test = callPackage ../tools/system/rowhammer-test { };