Add integration tests (#672)

This commit is contained in:
Karolin Varner
2025-09-03 15:52:39 +02:00
committed by GitHub
12 changed files with 1543 additions and 2 deletions

166
.github/workflows/integration.yml vendored Normal file
View File

@@ -0,0 +1,166 @@
name: Integration Tests
on:
pull_request:
push:
branches:
- main
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
integration-tests-x86_64-linux:
name: Integration tests x86_64-linux
runs-on:
- ubicloud-standard-2-ubuntu-2204
steps:
- uses: actions/checkout@v4
- uses: cachix/install-nix-action@v30
with:
nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@v15
with:
name: rosenpass
authToken: ${{ secrets.CACHIX_AUTH_TOKEN }}
- name: Extract the reference of before and after for the integration tests.
run: |
EVENT_NAME="${{ github.event_name }}"
REF_BEFORE=""
REF_AFTER="path:../../"
if [[ "$EVENT_NAME" == "pull_request" ]]; then
echo "This CI run was triggered in the context of a pull request."
REF_BEFORE="github:rosenpass/rosenpass/main"
git checkout -B pr-${{ github.event.pull_request.number }}
REF_AFTER="git+file://../../../?ref=pr-${{ github.event.pull_request.number }}"
elif [[ "$EVENT_NAME" == "push" ]]; then
echo "This CI run was triggered in the context of a push."
REF_BEFORE="github:rosenpass/rosenpass/${{ github.event.before }}"
REF_AFTER="github:rosenpass/rosenpass/${{ github.event.after }}"
else
echo "ERROR: This CI run was not triggered in the context of a pull request or a push. Exiting with error."
exit 1
fi
echo "REF_BEFORE=$REF_BEFORE" >> $GITHUB_ENV
echo "REF_AFTER=$REF_AFTER" >> $GITHUB_ENV
- name: Check
run: |
cd ./tests/integration
nix flake check --print-build-logs --system x86_64-linux --override-input rosenpass-old $REF_BEFORE --override-input rosenpass-new $REF_AFTER
# THE FOLLOWING TEST IS DISABLED FOR THE TIME BENG UNTIL WE GET AN ARM64 RUNNER THAT SUPPORTS KVM
#integration-tests-aarch64-linux:
# name: Integration tests aarch64-linux
# runs-on:
# - ubicloud-standard-2-arm-ubuntu-2204
# steps:
# - uses: actions/checkout@v4
# - uses: cachix/install-nix-action@v30
# with:
# nix_path: nixpkgs=channel:nixos-unstable
# - uses: cachix/cachix-action@v15
# with:
# name: rosenpass
# authToken: ${{ secrets.CACHIX_AUTH_TOKEN }}
# - name: Extract the reference of before and after for the integration tests.
# run: |
# EVENT_NAME="${{ github.event_name }}"
# REF_BEFORE=""
# REF_AFTER="path:../../"
# if [[ "$EVENT_NAME" == "pull_request" ]]; then
# echo "This CI run was triggered in the context of a pull request."
# REF_BEFORE="github:rosenpass/rosenpass/main"
# #git checkout -B pr-${{ github.event.pull_request.number }}
# REF_AFTER="git+file://../../../?ref=pr-${{ github.event.pull_request.number }}"
# elif [[ "$EVENT_NAME" == "push" ]]; then
# echo "This CI run was triggered in the context of a push."
# REF_BEFORE="github:rosenpass/rosenpass/${{ github.event.before }}"
# REF_AFTER="github:rosenpass/rosenpass/${{ github.event.after }}"
# #git checkout -B ${{ github.ref_name }}
# else
# echo "ERROR: This CI run was not triggered in the context of a pull request or a push. Exiting with error."
# exit 1
# fi
# echo "REF_BEFORE=$REF_BEFORE" >> $GITHUB_ENV
# echo "REF_AFTER=$REF_AFTER" >> $GITHUB_ENV
# - name: Check
# run: |
# cd ./tests/integration
# # export QEMU_OPTS="-machine virt -cpu cortex-a57"
# nix flake check --print-build-logs --system aarch64-linux --override-input rosenpass-old $REF_BEFORE --override-input rosenpass-new $REF_AFTER
#integration-tests-i686-linux:
# name: Integration tests i686-linux
# timeout-minutes: 144000
# runs-on:
# - ubicloud-standard-8-ubuntu-2204
# steps:
# - uses: actions/checkout@v4
# - uses: cachix/install-nix-action@v30
# with:
# nix_path: nixpkgs=channel:nixos-unstable
# - uses: cachix/cachix-action@v15
# with:
# name: rosenpass
# authToken: ${{ secrets.CACHIX_AUTH_TOKEN }}
# - name: Extract the reference of before and after for the integration tests.
# run: |
# EVENT_NAME="${{ github.event_name }}"
# REF_BEFORE=""
# REF_AFTER="path:../../"
# if [[ "$EVENT_NAME" == "pull_request" ]]; then
# echo "This CI run was triggered in the context of a pull request."
# REF_BEFORE="github:rosenpass/rosenpass/main"
# git checkout -B pr-${{ github.event.pull_request.number }}
# REF_AFTER="git+file://../../../?ref=pr-${{ github.event.pull_request.number }}"
# elif [[ "$EVENT_NAME" == "push" ]]; then
# echo "This CI run was triggered in the context of a push."
# REF_BEFORE="github:rosenpass/rosenpass/${{ github.event.before }}"
# REF_AFTER="github:rosenpass/rosenpass/${{ github.event.after }}"
# else
# echo "ERROR: This CI run was not triggered in the context of a pull request or a push. Exiting with error."
# exit 1
# fi
# echo "REF_BEFORE=$REF_BEFORE" >> $GITHUB_ENV
# echo "REF_AFTER=$REF_AFTER" >> $GITHUB_ENV
# - name: Check
# run: |
# cd ./tests/integration
# nix flake check --print-build-logs --system i686-linux --override-input rosenpass-old $REF_BEFORE --override-input rosenpass-new $REF_AFTER
# THE FOLLOWING TEST IS DISABLED FOR THE TIME BENG UNTIL THIS ISSUE WITH NIXOS TESTS ON DARWIN GETS RESOLVED: https://github.com/NixOS/nixpkgs/issues/294725
#integration-tests-aarch64-darwin:
# name: Integration tests aarch64-darwin
# runs-on:
# - warp-macos-13-arm64-6x
# steps:
# - uses: actions/checkout@v4
# - uses: cachix/install-nix-action@v30
# with:
# nix_path: nixpkgs=channel:nixos-unstable
# - uses: cachix/cachix-action@v15
# with:
# name: rosenpass
# authToken: ${{ secrets.CACHIX_AUTH_TOKEN }}
# - name: Extract the reference of before and after for the integration tests.
# run: |
# EVENT_NAME="${{ github.event_name }}"
# REF_BEFORE=""
# REF_AFTER="path:../../"
# if [[ "$EVENT_NAME" == "pull_request" ]]; then
# echo "This CI run was triggered in the context of a pull request."
# REF_BEFORE="github:rosenpass/rosenpass/main"
# git checkout -B pr-${{ github.event.pull_request.number }}
# REF_AFTER="git+file://../../../?ref=pr-${{ github.event.pull_request.number }}"
# elif [[ "$EVENT_NAME" == "push" ]]; then
# echo "This CI run was triggered in the context of a push."
# REF_BEFORE="github:rosenpass/rosenpass/${{ github.event.before }}"
# REF_AFTER="github:rosenpass/rosenpass/${{ github.event.after }}"
# else
# echo "ERROR: This CI run was not triggered in the context of a pull request or a push. Exiting with error."
# exit 1
# fi
# echo "REF_BEFORE=$REF_BEFORE" >> $GITHUB_ENV
# echo "REF_AFTER=$REF_AFTER" >> $GITHUB_ENV
# - name: Check
# run: |
# cd ./tests/integration
# nix flake check --print-build-logs --system aarch64-darwin --override-input rosenpass-old $REF_BEFORE --override-input rosenpass-new $REF_AFTER

View File

@@ -76,7 +76,9 @@ jobs:
name: rosenpass
authToken: ${{ secrets.CACHIX_AUTH_TOKEN }}
- name: Check
run: nix flake check . --print-build-logs
run: |
nix flake check . --print-build-logs
nix log /nix/store/3yvnay268gr9jpx3k1y2g3490j6ybssk-vm-test-run-rosenpass-with-key-exchangers.drv
x86_64-linux---default:
name: Build x86_64-linux.default
runs-on:

122
flake.lock generated
View File

@@ -18,6 +18,24 @@
"type": "github"
}
},
"flake-utils_2": {
"inputs": {
"systems": "systems_2"
},
"locked": {
"lastModified": 1726560853,
"narHash": "sha256-X6rJYSESBVr3hBoH0WbKE5KvhPU5bloyZ2L4K60/fPQ=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "c1dfcf08411b08f6b8615f7d8971a2bfa81d5e8a",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nix-vm-test": {
"inputs": {
"nixpkgs": [
@@ -38,6 +56,27 @@
"type": "github"
}
},
"nix-vm-test_2": {
"inputs": {
"nixpkgs": [
"rosenpassOld",
"nixpkgs"
]
},
"locked": {
"lastModified": 1734355073,
"narHash": "sha256-FfdPOGy1zElTwKzjgIMp5K2D3gfPn6VWjVa4MJ9L1Tc=",
"owner": "numtide",
"repo": "nix-vm-test",
"rev": "5948de39a616f2261dbbf4b6f25cbe1cbefd788c",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "nix-vm-test",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1728193676,
@@ -59,11 +98,58 @@
"flake-utils": "flake-utils",
"nix-vm-test": "nix-vm-test",
"nixpkgs": "nixpkgs",
"rosenpassOld": "rosenpassOld",
"rust-overlay": "rust-overlay_2",
"treefmt-nix": "treefmt-nix_2"
}
},
"rosenpassOld": {
"inputs": {
"flake-utils": "flake-utils_2",
"nix-vm-test": "nix-vm-test_2",
"nixpkgs": [
"nixpkgs"
],
"rust-overlay": "rust-overlay",
"treefmt-nix": "treefmt-nix"
},
"locked": {
"lastModified": 1754748821,
"narHash": "sha256-mMggTZDC97lLvKNOLtDz3GBjjxXFD++e1s0RZsVH/vI=",
"owner": "rosenpass",
"repo": "rosenpass",
"rev": "916a9ebb7133f0b22057fb097a473217f261928a",
"type": "github"
},
"original": {
"owner": "rosenpass",
"repo": "rosenpass",
"rev": "916a9ebb7133f0b22057fb097a473217f261928a",
"type": "github"
}
},
"rust-overlay": {
"inputs": {
"nixpkgs": [
"rosenpassOld",
"nixpkgs"
]
},
"locked": {
"lastModified": 1744513456,
"narHash": "sha256-NLVluTmK8d01Iz+WyarQhwFcXpHEwU7m5hH3YQQFJS0=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "730fd8e82799219754418483fabe1844262fd1e2",
"type": "github"
},
"original": {
"owner": "oxalica",
"repo": "rust-overlay",
"type": "github"
}
},
"rust-overlay_2": {
"inputs": {
"nixpkgs": [
"nixpkgs"
@@ -98,7 +184,43 @@
"type": "github"
}
},
"systems_2": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
},
"treefmt-nix": {
"inputs": {
"nixpkgs": [
"rosenpassOld",
"nixpkgs"
]
},
"locked": {
"lastModified": 1743748085,
"narHash": "sha256-uhjnlaVTWo5iD3LXics1rp9gaKgDRQj6660+gbUU3cE=",
"owner": "numtide",
"repo": "treefmt-nix",
"rev": "815e4121d6a5d504c0f96e5be2dd7f871e4fd99d",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "treefmt-nix",
"type": "github"
}
},
"treefmt-nix_2": {
"inputs": {
"nixpkgs": [
"nixpkgs"

View File

@@ -13,6 +13,10 @@
treefmt-nix.url = "github:numtide/treefmt-nix";
treefmt-nix.inputs.nixpkgs.follows = "nixpkgs";
# Older version of rosenpass, referenced here for backwards compatibility
rosenpassOld.url = "github:rosenpass/rosenpass?rev=916a9ebb7133f0b22057fb097a473217f261928a";
rosenpassOld.inputs.nixpkgs.follows = "nixpkgs";
};
outputs =
@@ -23,6 +27,7 @@
nix-vm-test,
rust-overlay,
treefmt-nix,
rosenpassOld,
...
}@inputs:
nixpkgs.lib.foldl (a: b: nixpkgs.lib.recursiveUpdate a b) { } [
@@ -183,7 +188,14 @@
};
checks =
{
import ./tests/integration/integration-checks.nix {
inherit system;
pkgs = inputs.nixpkgs;
lib = nixpkgs.lib;
rosenpassNew = self.packages.${system}.default;
rosenpassOld = rosenpassOld.packages.${system}.default;
}
// {
systemd-rosenpass = pkgs.testers.runNixOSTest ./tests/systemd/rosenpass.nix;
systemd-rp = pkgs.testers.runNixOSTest ./tests/systemd/rp.nix;
formatting = treefmtEval.config.build.check self;

View File

@@ -72,6 +72,8 @@ rustPlatform.buildRustPackage {
package
];
configFileVersion = "1";
doCheck = true;
cargoLock = {

View File

@@ -0,0 +1,29 @@
# Integration Tests
This directory contains integration tests for rosenpass in the form of a nix flake. Put simply, in order to run the integration tests for the main branch as they are on github right now, just run the following on a linux machine with nix installed and flakes enabled:
```
nix flake check
```
## Overview
The integration tests recognize two rosenpass versions, a new version and an old version. If not adapted, both are set to the version of the current main branch of rosenpass on github. We describe below how to change this.
All integration tests install rosenpass on virtual machines, run the key exchange, create a connection via wireguard that uses rosenpass and then checks whether all peers can ping each other via wireguard. Overall there are four integration tests:
- `basicConnectivity` -- This test only uses the new rosenpass version and checks whether the key exchange between two peers works such that they can ping each other.
- `backwardClient` -- This test is the same as the `basicConnectivity` test, but with the client using the old rosenpass version.
- `backwardServer` -- This test is the same as the `backwardClient` test, but with the server using the old rosenpass version.
- `multiPeer` -- This test again only uses the new rosenpass version, but with three peers. The first peer acts as a server towards the other two peers. The second peer acts as a client towards the first peer and as a server towards the third peer. The third peer acts as a client towards all peers.
## Testing specific versions
You can specify specific versions of rosenpass to test compatability. The proper way to do so is by overriding the respective inputs to the nix flake. As an example, say you want to test the compatability of your local version of rosenpass with the branch `new-feature` on github. You can achieve this by running the following command:
```
nix flake check --override-input rosenpass-old ../../ --override-input rosenpass-new github:rosenpass/rosenpass/new-feature
```
## Usage in the CI
In the CI, the integration tests are used differently, depending on whether the CI run is triggered by a push to the main branch or by a pull request. If the CI run is triggered by a pull request, then the result of merging the main branch and the PR branch is set as the new version and the current state of the main branch is set as the old version. For push events, the CI is only triggered if the push is onto the main branch. In that case, the state before the push event is considered the old version and the state after the push event is considered as the new version.

320
tests/integration/flake.lock generated Normal file
View File

@@ -0,0 +1,320 @@
{
"nodes": {
"flake-parts": {
"inputs": {
"nixpkgs-lib": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1751413152,
"narHash": "sha256-Tyw1RjYEsp5scoigs1384gIg6e0GoBVjms4aXFfRssQ=",
"owner": "hercules-ci",
"repo": "flake-parts",
"rev": "77826244401ea9de6e3bac47c2db46005e1f30b5",
"type": "github"
},
"original": {
"owner": "hercules-ci",
"repo": "flake-parts",
"type": "github"
}
},
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1726560853,
"narHash": "sha256-X6rJYSESBVr3hBoH0WbKE5KvhPU5bloyZ2L4K60/fPQ=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "c1dfcf08411b08f6b8615f7d8971a2bfa81d5e8a",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"flake-utils_2": {
"inputs": {
"systems": "systems_2"
},
"locked": {
"lastModified": 1726560853,
"narHash": "sha256-X6rJYSESBVr3hBoH0WbKE5KvhPU5bloyZ2L4K60/fPQ=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "c1dfcf08411b08f6b8615f7d8971a2bfa81d5e8a",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nix-vm-test": {
"inputs": {
"nixpkgs": [
"rosenpass-new",
"nixpkgs"
]
},
"locked": {
"lastModified": 1734355073,
"narHash": "sha256-FfdPOGy1zElTwKzjgIMp5K2D3gfPn6VWjVa4MJ9L1Tc=",
"owner": "numtide",
"repo": "nix-vm-test",
"rev": "5948de39a616f2261dbbf4b6f25cbe1cbefd788c",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "nix-vm-test",
"type": "github"
}
},
"nix-vm-test_2": {
"inputs": {
"nixpkgs": [
"rosenpass-old",
"nixpkgs"
]
},
"locked": {
"lastModified": 1734355073,
"narHash": "sha256-FfdPOGy1zElTwKzjgIMp5K2D3gfPn6VWjVa4MJ9L1Tc=",
"owner": "numtide",
"repo": "nix-vm-test",
"rev": "5948de39a616f2261dbbf4b6f25cbe1cbefd788c",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "nix-vm-test",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1735563628,
"narHash": "sha256-OnSAY7XDSx7CtDoqNh8jwVwh4xNL/2HaJxGjryLWzX8=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "b134951a4c9f3c995fd7be05f3243f8ecd65d798",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-24.05",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_2": {
"locked": {
"lastModified": 1728193676,
"narHash": "sha256-PbDWAIjKJdlVg+qQRhzdSor04bAPApDqIv2DofTyynk=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "ecbc1ca8ffd6aea8372ad16be9ebbb39889e55b6",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-24.05",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_3": {
"locked": {
"lastModified": 1728193676,
"narHash": "sha256-PbDWAIjKJdlVg+qQRhzdSor04bAPApDqIv2DofTyynk=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "ecbc1ca8ffd6aea8372ad16be9ebbb39889e55b6",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-24.05",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"flake-parts": "flake-parts",
"nixpkgs": "nixpkgs",
"rosenpass-new": "rosenpass-new",
"rosenpass-old": "rosenpass-old"
}
},
"rosenpass-new": {
"inputs": {
"flake-utils": "flake-utils",
"nix-vm-test": "nix-vm-test",
"nixpkgs": "nixpkgs_2",
"rust-overlay": "rust-overlay",
"treefmt-nix": "treefmt-nix"
},
"locked": {
"lastModified": 1752081615,
"narHash": "sha256-g9jC1HNCMSMPzArA8RCPGaxCCFH6dzQuq20RDsRwRT8=",
"owner": "rosenpass",
"repo": "rosenpass",
"rev": "3e03e479350551d11b81bde1bb55f5fdf8246f7c",
"type": "github"
},
"original": {
"owner": "rosenpass",
"ref": "main",
"repo": "rosenpass",
"type": "github"
}
},
"rosenpass-old": {
"inputs": {
"flake-utils": "flake-utils_2",
"nix-vm-test": "nix-vm-test_2",
"nixpkgs": "nixpkgs_3",
"rust-overlay": "rust-overlay_2",
"treefmt-nix": "treefmt-nix_2"
},
"locked": {
"lastModified": 1752081615,
"narHash": "sha256-g9jC1HNCMSMPzArA8RCPGaxCCFH6dzQuq20RDsRwRT8=",
"owner": "rosenpass",
"repo": "rosenpass",
"rev": "3e03e479350551d11b81bde1bb55f5fdf8246f7c",
"type": "github"
},
"original": {
"owner": "rosenpass",
"ref": "main",
"repo": "rosenpass",
"type": "github"
}
},
"rust-overlay": {
"inputs": {
"nixpkgs": [
"rosenpass-new",
"nixpkgs"
]
},
"locked": {
"lastModified": 1744513456,
"narHash": "sha256-NLVluTmK8d01Iz+WyarQhwFcXpHEwU7m5hH3YQQFJS0=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "730fd8e82799219754418483fabe1844262fd1e2",
"type": "github"
},
"original": {
"owner": "oxalica",
"repo": "rust-overlay",
"type": "github"
}
},
"rust-overlay_2": {
"inputs": {
"nixpkgs": [
"rosenpass-old",
"nixpkgs"
]
},
"locked": {
"lastModified": 1744513456,
"narHash": "sha256-NLVluTmK8d01Iz+WyarQhwFcXpHEwU7m5hH3YQQFJS0=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "730fd8e82799219754418483fabe1844262fd1e2",
"type": "github"
},
"original": {
"owner": "oxalica",
"repo": "rust-overlay",
"type": "github"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
},
"systems_2": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
},
"treefmt-nix": {
"inputs": {
"nixpkgs": [
"rosenpass-new",
"nixpkgs"
]
},
"locked": {
"lastModified": 1743748085,
"narHash": "sha256-uhjnlaVTWo5iD3LXics1rp9gaKgDRQj6660+gbUU3cE=",
"owner": "numtide",
"repo": "treefmt-nix",
"rev": "815e4121d6a5d504c0f96e5be2dd7f871e4fd99d",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "treefmt-nix",
"type": "github"
}
},
"treefmt-nix_2": {
"inputs": {
"nixpkgs": [
"rosenpass-old",
"nixpkgs"
]
},
"locked": {
"lastModified": 1743748085,
"narHash": "sha256-uhjnlaVTWo5iD3LXics1rp9gaKgDRQj6660+gbUU3cE=",
"owner": "numtide",
"repo": "treefmt-nix",
"rev": "815e4121d6a5d504c0f96e5be2dd7f871e4fd99d",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "treefmt-nix",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

View File

@@ -0,0 +1,46 @@
{
description = "Integration tests for rosenpass";
inputs = {
flake-parts.url = "github:hercules-ci/flake-parts";
flake-parts.inputs.nixpkgs-lib.follows = "nixpkgs";
nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.05";
# Override or change these inputs for testing new Integrations. They are overriden automatically when run in the CI
rosenpass-old.url = "github:rosenpass/rosenpass/main";
rosenpass-new.url = "github:rosenpass/rosenpass/main";
};
outputs =
inputs:
inputs.flake-parts.lib.mkFlake { inherit inputs; } {
systems = [
"i686-linux"
"x86_64-linux"
"aarch64-linux"
"aarch64-darwin"
];
perSystem =
{ system, lib, ... }:
let
# Since other parts of the CI are already doing the unit tests, we deactivate them here.
rosenpassOld = inputs.rosenpass-old.packages.${system}.default.overrideAttrs (old: {
doCheck = false;
});
rosenpassNew = inputs.rosenpass-new.packages.${system}.default.overrideAttrs (new: {
doCheck = false;
});
defaultChecks = import ./integration-checks.nix {
inherit
system
lib
rosenpassNew
rosenpassOld
;
pkgs = inputs.nixpkgs;
};
in
{
checks = defaultChecks;
};
};
}

View File

@@ -0,0 +1,111 @@
{
pkgs,
lib,
system,
rosenpassOld,
rosenpassNew,
...
}:
let
# The current version of ipython fails to build on i686 linux.
# We therefore pin an older version that works for the time beeing.
ipythonOverlay = final: prev: {
python313 = prev.python313.override {
packageOverrides = python-final: python-prev: {
ipython = python-prev.ipython.overridePythonAttrs (old: {
version = "8.37.0";
src = python-final.fetchPypi {
pname = "ipython";
version = "8.37.0";
hash = "sha256-yoFYQeGkGh5rc6CwjzA4r5siUlZNAfxAU1bTQDMBIhY=";
};
});
};
};
};
basicConnectivityOverlay = final: prev: {
rosenpass-peer-a = rosenpassNew;
rosenpass-peer-b = rosenpassNew;
};
backwardServerOverlay = final: prev: {
rosenpass-peer-a = rosenpassOld;
rosenpass-peer-b = rosenpassNew;
};
backwardClientOverlay = final: prev: {
rosenpass-peer-a = rosenpassNew;
rosenpass-peer-b = rosenpassOld;
};
multiPeerOverlay = final: prev: {
rosenpass-peer-a = rosenpassNew;
rosenpass-peer-b = rosenpassNew;
rosenpass-peer-c = rosenpassNew;
};
pkgsBasicConnectivity = import pkgs {
inherit system;
overlays = [
basicConnectivityOverlay
ipythonOverlay
];
};
pkgsBackwardServer = import pkgs {
inherit system;
overlays = [
backwardServerOverlay
ipythonOverlay
];
};
pkgsBackwardClient = import pkgs {
inherit system;
overlays = [
backwardClientOverlay
ipythonOverlay
];
};
pkgsMultiPeer = import pkgs {
inherit system;
overlays = [
multiPeerOverlay
ipythonOverlay
];
};
generatedChecks = {
basicConnectivity = pkgsBasicConnectivity.testers.runNixOSTest (
import ./rpsc-test.nix {
pkgs = pkgsBasicConnectivity;
inherit lib;
}
);
backwardServer = pkgsBackwardServer.testers.runNixOSTest (
import ./rpsc-test.nix {
pkgs = pkgsBackwardServer;
inherit lib;
}
);
backwardClient = pkgsBackwardClient.testers.runNixOSTest (
import ./rpsc-test.nix {
pkgs = pkgsBackwardClient;
inherit lib;
}
);
multiPeer = pkgsMultiPeer.testers.runNixOSTest (
import ./rpsc-test.nix {
pkgs = pkgsMultiPeer;
inherit lib;
multiPeer = true;
}
);
};
in
generatedChecks

View File

@@ -0,0 +1,43 @@
{
lib,
pkgs,
config,
...
}:
let
cfg = config.services.rosenpassKeyExchange;
in
{
options.services.rosenpassKeyExchange = {
create = lib.mkEnableOption "rosenpass key-exchange";
enable = lib.mkOption {
type = lib.types.bool;
description = "Should the service be enabled";
default = true;
};
config = lib.mkOption {
type = lib.types.path;
description = "Path to rosenpass configuration";
};
rosenpassVersion = lib.mkOption {
type = lib.types.package;
description = "Rosenpass package to use";
};
};
config = lib.mkIf cfg.create {
systemd.services.rp-exchange = {
description = "Rosenpass Key Exchanger";
wantedBy = [ ] ++ lib.optional cfg.enable "multi-user.target"; # If we set enable to this, then the service will be masked and cannot be enabled. Doing it this way allows us to enable it.
requires = [ "network-online.target" ];
script = ''
${cfg.rosenpassVersion}/bin/rosenpass exchange-config ${cfg.config}
'';
serviceConfig = {
Restart = "always";
RestartSec = 1;
};
};
};
}

View File

@@ -0,0 +1,104 @@
{
pkgs,
lib,
config,
...
}:
let
cfg = config.services.rosenpassKeySync;
servicePrefix = "rp-key-sync-";
timerPrefix = "rp-key-sync-timer-";
rpKeySyncOpts =
{ name, ... }:
{
# Each instance of ths service is defined by the following information:
options = {
create = lib.mkEnableOption "RP Keysync for ${name}";
enable = lib.mkOption {
type = lib.types.bool;
description = "Should the service be enabled";
default = true;
};
wgInterface = lib.mkOption {
type = lib.types.str;
description = "Wireguard interface name";
};
rpHost = lib.mkOption {
type = lib.types.str;
description = "network address of the host that runs rosenpass";
};
peerPubkeyFile = lib.mkOption {
type = lib.types.path;
description = "Public key of wireguard peer";
};
remoteKeyPath = lib.mkOption {
type = lib.types.path;
description = "Location of the .osk file on the key exchange server";
};
endpoint = lib.mkOption {
type = lib.types.str;
description = "IP address of the peer to connect via.";
};
allowedIps = lib.mkOption {
type = lib.types.str;
description = "IP addresses on the WireGuard VPN the peer is allowed to use";
};
};
};
in
{
options.services.rosenpassKeySync = {
instances = lib.mkOption {
type = lib.types.attrsOf (lib.types.submodule rpKeySyncOpts);
default = { };
description = "RP key sync instances";
};
};
config = {
systemd.services = lib.mapAttrs' (instanceName: instanceCfg: {
name = "${servicePrefix}${instanceName}";
value = {
description = "Rosenpass Key Downloader ${instanceName}";
wantedBy = [ ] ++ lib.optional instanceCfg.enable "multi-user.target"; # If we set enable to this, then the service will be masked and cannot be enabled. Doing it this way allows us to enable it.
requires = [ "network-online.target" ];
# The script downloads the key generated by rosenpass from the key exchange node and sets it as the preshared key for the specified wireguard peer.
script = ''
set -euo pipefail
PEER_PUB_KEY=$(cat ${instanceCfg.peerPubkeyFile})
${pkgs.openssh}/bin/ssh ${instanceCfg.rpHost} "cat ${instanceCfg.remoteKeyPath}" \
| ${pkgs.wireguard-tools}/bin/wg \
set ${instanceCfg.wgInterface} \
peer $PEER_PUB_KEY \
endpoint ${instanceCfg.endpoint} \
allowed-ips ${instanceCfg.allowedIps} \
preshared-key /dev/stdin
'';
serviceConfig = {
Restart = "always";
RestartSec = 10;
};
};
}) (lib.filterAttrs (_: cfg: cfg.create) cfg.instances); # this creates one systemd service (as above) per configured instance.
systemd.timers = lib.mapAttrs' (instanceName: instanceCfg: {
name = "${timerPrefix}${instanceName}";
value = {
wantedBy = [ "timers.target" ];
timerConfig = {
requires = [ "network-online.target" ];
OnUnitActiveSec = "1m";
Unit = "${servicePrefix}${instanceName}.service";
};
};
}) (lib.filterAttrs (_: cfg: cfg.create) cfg.instances); # this creates one systemd timer (as above) per configured instance.
};
}

View File

@@ -0,0 +1,584 @@
{
pkgs,
lib,
multiPeer ? false,
...
}:
let
wgInterface = "mywg";
wgPort = 51820;
rpPort = 51821;
rosenpassKeyFolder = "/var/secrets";
wireguardKeyFolder = "/var/wgKeys";
keyExchangePathAB = "/root/peer-ab.osk";
keyExchangePathBA = "/root/peer-ba.osk";
keyExchangePathAC = "/root/peer-ac.osk";
keyExchangePathCA = "/root/peer-ca.osk";
keyExchangePathBC = "/root/peer-bc.osk";
keyExchangePathCB = "/root/peer-cb.osk";
getConfigFileVersion =
rosenpassVersion:
let
configFileVersion =
if builtins.hasAttr "configFileVersion" rosenpassVersion then
rosenpassVersion.configFileVersion
else
"0";
in
configFileVersion;
peerAConfigFileVersion = getConfigFileVersion pkgs.rosenpass-peer-a;
peerBConfigFileVersion = getConfigFileVersion pkgs.rosenpass-peer-b;
peerCConfigFileVersion = if multiPeer then getConfigFileVersion pkgs.rosenpass-peer-c else null;
staticConfig =
{
peerA = {
innerIp = "10.100.0.1";
wgPrivateKeyFile = "${wireguardKeyFolder}/peerA.sk";
wgPublicKeyFile = "${wireguardKeyFolder}/peerA.pk";
rosenpassConfig = builtins.toFile "peer-a.toml" (
''
public_key = "${rosenpassKeyFolder}/self.pk"
secret_key = "${rosenpassKeyFolder}/self.sk"
listen = ["[::]:${builtins.toString rpPort}"]
verbosity = "Verbose"
[[peers]]
public_key = "${rosenpassKeyFolder}/peer-b.pk"
endpoint = "peerbkeyexchanger:${builtins.toString rpPort}"
key_out = "${keyExchangePathAB}"
''
+ (lib.optionalString multiPeer ''
[[peers]]
public_key = "${rosenpassKeyFolder}/peer-c.pk"
endpoint = "peerckeyexchanger:${builtins.toString rpPort}"
key_out = "${keyExchangePathAC}"
'')
);
};
peerB = {
innerIp = "10.100.0.2";
wgPrivateKeyFile = "${wireguardKeyFolder}/peerB.sk";
wgPublicKeyFile = "${wireguardKeyFolder}/peerB.pk";
rosenpassConfig = builtins.toFile "peer-b.toml" (
''
public_key = "${rosenpassKeyFolder}/self.pk"
secret_key = "${rosenpassKeyFolder}/self.sk"
listen = ["[::]:${builtins.toString rpPort}"]
verbosity = "Verbose"
[[peers]]
public_key = "${rosenpassKeyFolder}/peer-a.pk"
endpoint = "peerakeyexchanger:${builtins.toString rpPort}"
key_out = "${keyExchangePathBA}"
''
+ (lib.optionalString multiPeer ''
[[peers]]
public_key = "${rosenpassKeyFolder}/peer-c.pk"
endpoint = "peerckeyexchanger:${builtins.toString rpPort}"
key_out = "${keyExchangePathBC}"
'')
);
};
}
// lib.optionalAttrs multiPeer {
# peerC is only defined if we are in a multiPeer context.
peerC = {
innerIp = "10.100.0.3";
wgPrivateKeyFile = "${wireguardKeyFolder}/peerC.sk";
wgPublicKeyFile = "${wireguardKeyFolder}/peerC.pk";
rosenpassConfig = builtins.toFile "peer-c.toml" ''
public_key = "${rosenpassKeyFolder}/self.pk"
secret_key = "${rosenpassKeyFolder}/self.sk"
listen = ["[::]:${builtins.toString rpPort}"]
verbosity = "Verbose"
[[peers]]
public_key = "${rosenpassKeyFolder}/peer-a.pk"
endpoint = "peerakeyexchanger:${builtins.toString rpPort}"
key_out = "${keyExchangePathCA}"
[[peers]]
public_key = "${rosenpassKeyFolder}/peer-b.pk"
endpoint = "peerckeyexchanger:${builtins.toString rpPort}"
key_out = "${keyExchangePathCB}"
'';
};
};
inherit (import (pkgs.path + "/nixos/tests/ssh-keys.nix") pkgs)
snakeOilPublicKey
snakeOilPrivateKey
;
# All hosts in this scenario use the same key pair
# The script takes the host as parameter and prepares passwordless login
prepareSshLogin = pkgs.writeShellScriptBin "prepare-ssh-login" ''
set -euo pipefail
mkdir -p /root/.ssh
cp ${snakeOilPrivateKey} /root/.ssh/id_ecdsa
chmod 0400 /root/.ssh/id_ecdsa
${pkgs.openssh}/bin/ssh -o StrictHostKeyChecking=no "$1" true
'';
in
{
name = "rosenpass with key exchangers";
defaults = {
imports = [
./rp-key-exchange.nix
./rp-key-sync.nix
];
systemd.tmpfiles.rules = [ "d ${rosenpassKeyFolder} 0400 root root - -" ];
};
nodes =
{
# peerA and peerB are the only neccessary peers unless we are in the multiPeer test.
peerA = {
networking.firewall.allowedUDPPorts = [ wgPort ];
# Each instance of the key sync service loads a symmetric key from a rosenpass keyexchanger node and sets it as the preshared key for the appropriate wireguard tunnel.
services.rosenpassKeySync.instances =
{
AB = {
create = true;
enable = false;
inherit wgInterface;
rpHost = "peerakeyexchanger";
peerPubkeyFile = staticConfig.peerB.wgPublicKeyFile;
remoteKeyPath = keyExchangePathAB;
endpoint = "peerB:${builtins.toString wgPort}";
allowedIps = "${staticConfig.peerB.innerIp}/32";
};
}
// lib.optionalAttrs multiPeer {
AC = {
create = true;
enable = false;
inherit wgInterface;
rpHost = "peerakeyexchanger";
peerPubkeyFile = staticConfig.peerC.wgPublicKeyFile;
remoteKeyPath = keyExchangePathAC;
endpoint = "peerC:${builtins.toString wgPort}";
allowedIps = "${staticConfig.peerC.innerIp}/32";
};
};
};
peerB = {
networking.firewall.allowedUDPPorts = [ wgPort ];
# Each instance of the key sync service loads a symmetric key from a rosenpass keyexchanger node and sets it as the preshared key for the appropriate wireguard tunnel.
services.rosenpassKeySync.instances =
{
BA = {
create = true;
enable = false;
inherit wgInterface;
rpHost = "peerbkeyexchanger";
peerPubkeyFile = staticConfig.peerA.wgPublicKeyFile;
remoteKeyPath = keyExchangePathBA;
endpoint = "peerA:${builtins.toString wgPort}";
allowedIps = "${staticConfig.peerA.innerIp}/32";
};
}
// lib.optionalAttrs multiPeer {
BC = {
create = true;
enable = false;
inherit wgInterface;
rpHost = "peerbkeyexchanger";
peerPubkeyFile = staticConfig.peerC.wgPublicKeyFile;
remoteKeyPath = keyExchangePathBC;
endpoint = "peerC:${builtins.toString wgPort}";
allowedIps = "${staticConfig.peerC.innerIp}/32";
};
};
};
# The key exchanger node for peerA is the node that actually runs rosenpass. It takes the rosenpass confguration for peerA and runs it.
# The key sync services of peerA will ssh into this node and download the exchanged keys from here.
peerakeyexchanger = {
services.openssh.enable = true;
users.users.root.openssh.authorizedKeys.keys = [ snakeOilPublicKey ];
networking.firewall.allowedUDPPorts = [ rpPort ];
services.rosenpassKeyExchange = {
create = true;
enable = false;
config = staticConfig.peerA.rosenpassConfig;
rosenpassVersion = pkgs.rosenpass-peer-a;
};
};
# The key exchanger node for peerB is the node that actually runs rosenpass. It takes the rosenpass confguration for peerB and runs it.
# The key sync services of peerB will ssh into this node and download the exchanged keys from here.
peerbkeyexchanger = {
services.openssh.enable = true;
users.users.root.openssh.authorizedKeys.keys = [ snakeOilPublicKey ];
services.rosenpassKeyExchange = {
create = true;
enable = false;
config = staticConfig.peerB.rosenpassConfig;
rosenpassVersion = pkgs.rosenpass-peer-b;
};
};
}
// lib.optionalAttrs multiPeer {
peerC = {
networking.firewall.allowedUDPPorts = [ wgPort ];
# Each instance of the key sync service loads a symmetric key from a rosenpass keyexchanger node and sets it as the preshared key for the appropriate wireguard tunnel.
services.rosenpassKeySync.instances = {
CA = {
create = true;
enable = false;
inherit wgInterface;
rpHost = "peerckeyexchanger";
peerPubkeyFile = staticConfig.peerA.wgPublicKeyFile;
remoteKeyPath = keyExchangePathCA;
endpoint = "peerA:${builtins.toString wgPort}";
allowedIps = "${staticConfig.peerA.innerIp}/32";
};
CB = {
create = true;
enable = false;
inherit wgInterface;
rpHost = "peerckeyexchanger";
peerPubkeyFile = staticConfig.peerB.wgPublicKeyFile;
remoteKeyPath = keyExchangePathCB;
endpoint = "peerB:${builtins.toString wgPort}";
allowedIps = "${staticConfig.peerB.innerIp}/32";
};
};
};
# The key exchanger node for peerC is the node that actually runs rosenpass. It takes the rosenpass confguration for peerC and runs it.
# The key sync services of peerC will ssh into this node and download the exchanged keys from here.
peerckeyexchanger = {
services.openssh.enable = true;
users.users.root.openssh.authorizedKeys.keys = [ snakeOilPublicKey ];
networking.firewall.allowedUDPPorts = [ rpPort ];
services.rosenpassKeyExchange = {
create = true;
enable = false;
config = staticConfig.peerC.rosenpassConfig;
rosenpassVersion = pkgs.rosenpass-peer-c;
};
};
};
interactive = {
defaults = {
users.extraUsers.root.initialPassword = "";
services.openssh = {
enable = true;
settings = {
PermitRootLogin = "yes";
PermitEmptyPasswords = "yes";
};
};
security.pam.services.sshd.allowNullPassword = true;
environment.systemPackages = [
prepareSshLogin
(pkgs.writeShellScriptBin "watch-wg" ''
${pkgs.procps}/bin/watch -n1 \
${pkgs.wireguard-tools}/bin/wg show all preshared-keys
'')
];
};
nodes.peerA = {
virtualisation.forwardPorts = [
{
from = "host";
host.port = 2222;
guest.port = 22;
}
];
};
nodes.peerB = {
virtualisation.forwardPorts = [
{
from = "host";
host.port = 2223;
guest.port = 22;
}
];
};
nodes.peerC = {
virtualisation.forwardPorts = [
{
from = "host";
host.port = 2224;
guest.port = 22;
}
];
};
};
testScript = (''
start_all()
print("""Config file versions supported by peers
peerA: ${peerAConfigFileVersion}
peerB: ${peerBConfigFileVersion}
${lib.optionalString multiPeer ''
peerC: ${peerCConfigFileVersion}
''}
""")
for m in [peerA, peerB, peerakeyexchanger, peerbkeyexchanger]:
m.wait_for_unit("network-online.target")
${lib.optionalString multiPeer ''
for m in [peerC, peerckeyexchanger]:
m.wait_for_unit("network-online.target")
''}
# Generate the normal wireguard key pairs
peerA.succeed("mkdir ${wireguardKeyFolder}")
peerA.succeed("${pkgs.wireguard-tools}/bin/wg genkey > ${staticConfig.peerA.wgPrivateKeyFile}")
peerA.succeed("cat ${staticConfig.peerA.wgPrivateKeyFile} | ${pkgs.wireguard-tools}/bin/wg pubkey > ${staticConfig.peerA.wgPublicKeyFile}")
peerAWgSk = peerA.succeed("cat ${staticConfig.peerA.wgPrivateKeyFile} | tr -d '\n'")
peerAWgPk = peerA.succeed("cat ${staticConfig.peerA.wgPublicKeyFile} | tr -d '\n'")
peerA.succeed("echo -n AR/yvSvMAzW6eS27PsRHUMWwC8cLhaD96t42cysxrb0= > ${wireguardKeyFolder}/peerB.psk")
peerB.succeed("mkdir ${wireguardKeyFolder}")
peerB.succeed("${pkgs.wireguard-tools}/bin/wg genkey > ${staticConfig.peerB.wgPrivateKeyFile}")
peerB.succeed("cat ${staticConfig.peerB.wgPrivateKeyFile} | ${pkgs.wireguard-tools}/bin/wg pubkey > ${staticConfig.peerB.wgPublicKeyFile}")
peerBWgSk = peerB.succeed("cat ${staticConfig.peerB.wgPrivateKeyFile} | tr -d '\n'")
peerBWgPk = peerB.succeed("cat ${staticConfig.peerB.wgPublicKeyFile} | tr -d '\n'")
peerB.succeed("echo -n o25fjoIOI623cnRyhvD4YEGtuSY4BFRZmY3UHvZ0BCA= > ${wireguardKeyFolder}/peerA.psk")
${lib.optionalString multiPeer ''
peerC.succeed("mkdir ${wireguardKeyFolder}")
peerC.succeed("${pkgs.wireguard-tools}/bin/wg genkey > ${staticConfig.peerC.wgPrivateKeyFile}")
peerC.succeed("cat ${staticConfig.peerC.wgPrivateKeyFile} | ${pkgs.wireguard-tools}/bin/wg pubkey > ${staticConfig.peerC.wgPublicKeyFile}")
peerCWgSk = peerC.succeed("cat ${staticConfig.peerC.wgPrivateKeyFile} | tr -d '\n'")
peerCWgPk = peerC.succeed("cat ${staticConfig.peerC.wgPublicKeyFile} | tr -d '\n'")
peerA.succeed("echo -n LfWvJCN8h7NhS+JWRG7GMIY20JxUV4WUs7MJ45ZGoCE= > ${wireguardKeyFolder}/peerC.psk")
peerB.succeed("echo -n GsYTUd/4Ph7wMy5r+W1no9yGe0UeZlmCPeiyu4tb6yM= > ${wireguardKeyFolder}/peerC.psk")
peerC.succeed("echo -n s9aIG1pY6nj2lH6p61tP8WRETNgQvoTfgel5BmVjYeI= > ${wireguardKeyFolder}/peerA.psk")
peerC.succeed("echo -n DYlFqWg/M6EfnMolBO+b4DFNrRyS6YWr4lM/2xRE1FQ= > ${wireguardKeyFolder}/peerB.psk")
''}
# Distribute the respective public keys
peerA.succeed(f"echo -n {peerBWgPk} > ${wireguardKeyFolder}/peerB.pk")
peerB.succeed(f"echo -n {peerAWgPk} > ${wireguardKeyFolder}/peerA.pk")
${lib.optionalString multiPeer ''
peerA.succeed(f"echo -n {peerCWgPk} > ${wireguardKeyFolder}/peerC.pk")
peerB.succeed(f"echo -n {peerCWgPk} > ${wireguardKeyFolder}/peerC.pk")
peerC.succeed(f"echo -n {peerAWgPk} > ${wireguardKeyFolder}/peerA.pk")
peerC.succeed(f"echo -n {peerBWgPk} > ${wireguardKeyFolder}/peerB.pk")
''}
# Make the wireguard public keys readable for the key-sync service.
peerA.succeed("chmod -R 0555 ${wireguardKeyFolder}")
peerB.succeed("chmod -R 0555 ${wireguardKeyFolder}")
${lib.optionalString multiPeer ''
peerC.succeed("chmod -R 0555 ${wireguardKeyFolder}")
''}
# Set up wireguard on peerA
peerA.succeed("ip link add ${wgInterface} type wireguard")
peerA.succeed("${pkgs.wireguard-tools}/bin/wg set ${wgInterface} private-key ${staticConfig.peerA.wgPrivateKeyFile} listen-port ${builtins.toString wgPort}")
peerA.succeed(f"${pkgs.wireguard-tools}/bin/wg set ${wgInterface} peer {peerBWgPk} allowed-ips ${staticConfig.peerB.innerIp}/32 endpoint peerB:${builtins.toString wgPort} preshared-key ${wireguardKeyFolder}/peerB.psk")
${lib.optionalString multiPeer ''
peerA.succeed(f"${pkgs.wireguard-tools}/bin/wg set ${wgInterface} peer {peerCWgPk} allowed-ips ${staticConfig.peerC.innerIp}/32 endpoint peerC:${builtins.toString wgPort} preshared-key ${wireguardKeyFolder}/peerC.psk")
''}
peerA.succeed("ip addr add ${staticConfig.peerA.innerIp}/32 dev ${wgInterface}")
peerA.succeed("ip link set ${wgInterface} up")
peerA.succeed("ip route add ${staticConfig.peerB.innerIp} dev ${wgInterface} scope link")
${lib.optionalString multiPeer ''
peerA.succeed("ip route add ${staticConfig.peerC.innerIp} dev ${wgInterface} scope link")
''}
# Set up wireguard on peerB
peerB.succeed("ip link add ${wgInterface} type wireguard")
peerB.succeed("${pkgs.wireguard-tools}/bin/wg set ${wgInterface} private-key ${staticConfig.peerB.wgPrivateKeyFile} listen-port ${builtins.toString wgPort}")
peerB.succeed(f"${pkgs.wireguard-tools}/bin/wg set ${wgInterface} peer {peerAWgPk} allowed-ips ${staticConfig.peerA.innerIp}/32 endpoint peerA:${builtins.toString wgPort} preshared-key ${wireguardKeyFolder}/peerA.psk")
${lib.optionalString multiPeer ''
peerB.succeed(f"${pkgs.wireguard-tools}/bin/wg set ${wgInterface} peer {peerCWgPk} allowed-ips ${staticConfig.peerC.innerIp}/32 endpoint peerC:${builtins.toString wgPort} preshared-key ${wireguardKeyFolder}/peerC.psk")
''}
peerB.succeed("ip addr add ${staticConfig.peerB.innerIp}/32 dev ${wgInterface}")
peerB.succeed("ip link set ${wgInterface} up")
peerB.succeed("ip route add ${staticConfig.peerA.innerIp} dev ${wgInterface} scope link")
${lib.optionalString multiPeer ''
peerB.succeed("ip route add ${staticConfig.peerC.innerIp} dev ${wgInterface} scope link")
''}
# Set up wireguard on peerC
${lib.optionalString multiPeer ''
peerC.succeed("ip link add ${wgInterface} type wireguard")
peerC.succeed("${pkgs.wireguard-tools}/bin/wg set ${wgInterface} private-key ${staticConfig.peerC.wgPrivateKeyFile} listen-port ${builtins.toString wgPort}")
peerC.succeed(f"${pkgs.wireguard-tools}/bin/wg set ${wgInterface} peer {peerAWgPk} allowed-ips ${staticConfig.peerA.innerIp}/32 endpoint peerA:${builtins.toString wgPort} preshared-key ${wireguardKeyFolder}/peerA.psk")
peerC.succeed(f"${pkgs.wireguard-tools}/bin/wg set ${wgInterface} peer {peerBWgPk} allowed-ips ${staticConfig.peerB.innerIp}/32 endpoint peerB:${builtins.toString wgPort} preshared-key ${wireguardKeyFolder}/peerB.psk")
peerC.succeed("ip addr add ${staticConfig.peerC.innerIp}/32 dev ${wgInterface}")
peerC.succeed("ip link set ${wgInterface} up")
peerC.succeed("ip route add ${staticConfig.peerA.innerIp} dev ${wgInterface} scope link")
peerC.succeed("ip route add ${staticConfig.peerB.innerIp} dev ${wgInterface} scope link")
''}
def debugPrintNetState():
# Dump current state of WireGuard tunnels
peerA.succeed("${pkgs.wireguard-tools}/bin/wg show all 1>&2")
peerB.succeed("${pkgs.wireguard-tools}/bin/wg show all 1>&2")
${lib.optionalString multiPeer ''
peerC.succeed("${pkgs.wireguard-tools}/bin/wg show all 1>&2")
''}
peerA.succeed("${pkgs.wireguard-tools}/bin/wg show all preshared-keys 1>&2")
peerB.succeed("${pkgs.wireguard-tools}/bin/wg show all preshared-keys 1>&2")
${lib.optionalString multiPeer ''
peerC.succeed("${pkgs.wireguard-tools}/bin/wg show all preshared-keys 1>&2")
''}
# Dump current network config
peerA.succeed("ip addr 1>&2")
peerA.succeed("ip route 1>&2")
peerakeyexchanger.succeed("ip addr 1>&2")
peerakeyexchanger.succeed("ip route 1>&2")
peerB.succeed("ip addr 1>&2")
peerB.succeed("ip route 1>&2")
peerbkeyexchanger.succeed("ip addr 1>&2")
peerbkeyexchanger.succeed("ip route 1>&2")
${lib.optionalString multiPeer ''
peerC.succeed("ip addr 1>&2")
peerC.succeed("ip route 1>&2")
peerckeyexchanger.succeed("ip addr 1>&2")
peerckeyexchanger.succeed("ip route 1>&2")
''}
debugPrintNetState()
# The wireguard connection can't work because the sync services fail on
# non-recognized SSH host keys, we didn't deploy the secrets and because the preshared keyes don't match.
peerB.fail("ping -W 2 -c 1 ${staticConfig.peerA.innerIp}")
peerA.fail("ping -W 2 -c 1 ${staticConfig.peerB.innerIp}")
${lib.optionalString multiPeer ''
peerA.fail("ping -W 2 -c 1 ${staticConfig.peerC.innerIp}")
peerB.fail("ping -W 2 -c 1 ${staticConfig.peerC.innerIp}")
peerC.fail("ping -W 2 -c 1 ${staticConfig.peerA.innerIp}")
peerC.fail("ping -W 2 -c 1 ${staticConfig.peerB.innerIp}")
''}
# In admin-reality, this should be done with your favorite secret
# provisioning/deployment tool
# In reality, admins would carefully manage known SSH host keys with
# their favorite secret provisioning/deployment tool
peerA.succeed("${prepareSshLogin}/bin/prepare-ssh-login peerakeyexchanger")
peerB.succeed("${prepareSshLogin}/bin/prepare-ssh-login peerbkeyexchanger")
${lib.optionalString multiPeer ''
peerC.succeed("${prepareSshLogin}/bin/prepare-ssh-login peerckeyexchanger")
''}
peerakeyexchanger.succeed("${prepareSshLogin}/bin/prepare-ssh-login peerbkeyexchanger")
peerbkeyexchanger.succeed("${prepareSshLogin}/bin/prepare-ssh-login peerakeyexchanger")
${lib.optionalString multiPeer ''
peerakeyexchanger.succeed("${prepareSshLogin}/bin/prepare-ssh-login peerckeyexchanger")
peerbkeyexchanger.succeed("${prepareSshLogin}/bin/prepare-ssh-login peerckeyexchanger")
peerckeyexchanger.succeed("${prepareSshLogin}/bin/prepare-ssh-login peerakeyexchanger")
peerckeyexchanger.succeed("${prepareSshLogin}/bin/prepare-ssh-login peerbkeyexchanger")
''}
# Generate the rosenpass key pairs.
peerakeyexchanger.succeed(
"${pkgs.rosenpass-peer-a}/bin/rosenpass gen-keys -p ${rosenpassKeyFolder}/self.pk -s ${rosenpassKeyFolder}/self.sk"
)
peerbkeyexchanger.succeed(
"${pkgs.rosenpass-peer-b}/bin/rosenpass gen-keys -p ${rosenpassKeyFolder}/self.pk -s ${rosenpassKeyFolder}/self.sk"
)
${lib.optionalString multiPeer ''
peerckeyexchanger.succeed(
"${pkgs.rosenpass-peer-c}/bin/rosenpass gen-keys -p ${rosenpassKeyFolder}/self.pk -s ${rosenpassKeyFolder}/self.sk"
)
''}
peerakeyexchanger.succeed(
"scp ${rosenpassKeyFolder}/self.pk peerbkeyexchanger:${rosenpassKeyFolder}/peer-a.pk"
)
peerbkeyexchanger.succeed(
"scp ${rosenpassKeyFolder}/self.pk peerakeyexchanger:${rosenpassKeyFolder}/peer-b.pk"
)
${lib.optionalString multiPeer ''
peerakeyexchanger.succeed(
"scp ${rosenpassKeyFolder}/self.pk peerckeyexchanger:${rosenpassKeyFolder}/peer-a.pk"
)
peerbkeyexchanger.succeed(
"scp ${rosenpassKeyFolder}/self.pk peerckeyexchanger:${rosenpassKeyFolder}/peer-b.pk"
)
peerckeyexchanger.succeed(
"scp ${rosenpassKeyFolder}/self.pk peerakeyexchanger:${rosenpassKeyFolder}/peer-c.pk"
)
peerckeyexchanger.succeed(
"scp ${rosenpassKeyFolder}/self.pk peerbkeyexchanger:${rosenpassKeyFolder}/peer-c.pk"
)
''}
# Until now, the services were disbaled and didn't start (using the enable option of the services)
peerakeyexchanger.succeed("systemctl start rp-exchange.service")
peerbkeyexchanger.succeed("systemctl start rp-exchange.service")
${lib.optionalString multiPeer ''
peerckeyexchanger.succeed("systemctl start rp-exchange.service")
''}
# Wait for the service to have started.
for m in [peerbkeyexchanger, peerakeyexchanger]:
m.wait_for_unit("rp-exchange.service")
${lib.optionalString multiPeer ''
peerckeyexchanger.wait_for_unit("rp-exchange.service")
''}
debugPrintNetState()
# Start key sync services and wait for them to start.
peerA.succeed("systemctl start rp-key-sync-AB.service")
peerB.succeed("systemctl start rp-key-sync-BA.service")
${lib.optionalString multiPeer ''
peerA.succeed("systemctl start rp-key-sync-AC.service")
peerB.succeed("systemctl start rp-key-sync-BC.service")
peerC.succeed("systemctl start rp-key-sync-CA.service")
peerC.succeed("systemctl start rp-key-sync-CB.service")
''}
peerA.wait_for_unit("rp-key-sync-AB.service")
peerB.wait_for_unit("rp-key-sync-BA.service")
${lib.optionalString multiPeer ''
peerA.wait_for_unit("rp-key-sync-AC.service")
peerB.wait_for_unit("rp-key-sync-BC.service")
peerC.wait_for_unit("rp-key-sync-CA.service")
peerC.wait_for_unit("rp-key-sync-CB.service")
''}
debugPrintNetState()
# Voila!
peerB.succeed("ping -c 1 -W 10 ${staticConfig.peerA.innerIp}")
${lib.optionalString multiPeer ''
peerC.succeed("ping -c 1 -W 10 ${staticConfig.peerA.innerIp}")
peerC.succeed("ping -c 1 -W 10 ${staticConfig.peerB.innerIp}")
peerA.succeed("ping -c 1 -W 10 ${staticConfig.peerC.innerIp}")
peerB.succeed("ping -c 1 -W 10 ${staticConfig.peerC.innerIp}")
''}
peerA.succeed("ping -c 1 -W 10 ${staticConfig.peerB.innerIp}")
# Dump current state of WireGuard tunnels
peerA.succeed("${pkgs.wireguard-tools}/bin/wg show all 1>&2")
peerB.succeed("${pkgs.wireguard-tools}/bin/wg show all 1>&2")
${lib.optionalString multiPeer ''
peerC.succeed("${pkgs.wireguard-tools}/bin/wg show all 1>&2")
''}
peerA.succeed("${pkgs.wireguard-tools}/bin/wg show all preshared-keys 1>&2")
peerB.succeed("${pkgs.wireguard-tools}/bin/wg show all preshared-keys 1>&2")
${lib.optionalString multiPeer ''
peerC.succeed("${pkgs.wireguard-tools}/bin/wg show all preshared-keys 1>&2")
''}
'');
}