nixos/reaction: test core plugins

Co-authored-by: ppom <38916722+ppom0@users.noreply.github.com>
Signed-off-by: phanirithvij <phanirithvij2000@gmail.com>
This commit is contained in:
phanirithvij 2026-02-20 11:16:44 +05:30
commit 4d4b662ab1
No known key found for this signature in database
4 changed files with 200 additions and 6 deletions

View file

@ -12,12 +12,6 @@
stopForFirewall = false;
# example.jsonnet or example.yml can be copied and modified from ${pkgs.reaction}/share/examples
settingsFiles = [ "${pkgs.reaction}/share/examples/example.jsonnet" ];
settings = {
# In the qemu vm `run0 ls` as root prints nothing, so we can't use it
# see https://reaction.ppom.me/reference.html#systemd
plugins.ipset.systemd = false;
plugins.virtual.systemd = false;
};
runAsRoot = false;
};
services.openssh.enable = true;

View file

@ -2,4 +2,5 @@
lib.recurseIntoAttrs {
basic = runTest ./basic.nix;
firewall = runTest ./firewall.nix;
plugins = runTest ./plugins.nix;
}

View file

@ -0,0 +1,125 @@
{
lib,
pkgs,
...
}:
{
name = "reaction-core-plugins";
nodes.server = args: {
services.reaction = {
enable = true;
stopForFirewall = false;
runAsRoot = false;
settings = import ./settings.nix args;
/*
# NOTE: When runAsRoot is true, disable run0
settings = {
# In the qemu vm `run0 ls` as root prints nothing, so we can't use it
# see https://reaction.ppom.me/reference.html#systemd
plugins.ipset.systemd = false;
plugins.virtual.systemd = false;
};
*/
};
/*
NOTE:
- if reaction is run as non-root, the plugins need these capabilities, remove these if runAsRoot is true
- CAP_DAC_READ_SEARCH is for journalctl for accessing ssh logs
- useful tools: capable (from package bcc), captree, getpcaps (from libpcap)
*/
systemd.services.reaction.serviceConfig = {
CapabilityBoundingSet = [
"CAP_NET_ADMIN"
"CAP_NET_RAW"
"CAP_DAC_READ_SEARCH"
];
AmbientCapabilities = [
"CAP_NET_ADMIN"
"CAP_NET_RAW"
"CAP_DAC_READ_SEARCH"
];
};
services.openssh.enable = true;
users.users.nixos.isNormalUser = true; # neeeded to establish a ssh connection, by default root login is succeeding without any password
};
nodes.client = _: {
environment.systemPackages = [
pkgs.sshpass
pkgs.libressl.nc
];
};
testScript =
{ nodes, ... }: # py
''
start_all()
# Wait for everything to be ready.
server.wait_for_unit("multi-user.target")
server.wait_for_unit("reaction")
server.wait_for_unit("sshd")
client_addr = "${(lib.head nodes.client.networking.interfaces.eth1.ipv4.addresses).address}"
server_addr = "${(lib.head nodes.server.networking.interfaces.eth1.ipv4.addresses).address}"
# Verify there is not ban and the port is reachable from the client.
server.succeed(f"reaction show | grep -q {client_addr} || test $? -eq 1")
client.succeed(f"nc -w3 -z {server_addr} 22")
# Cause authentication failure log entries.
for _ in range(2):
client.fail(f"""
sshpass -p 'wrongpassword' \
ssh -o StrictHostKeyChecking=no \
-o User=nixos \
-o ServerAliveInterval=1 \
-o ServerAliveCountMax=2 \
{server_addr}
""")
# Verify there is a ban and the port is unreachable from the client.
server.sleep(2)
output = server.succeed("reaction show")
print(output)
assert client_addr in output, f"client did not get banned, {client_addr}"
client.fail(f"nc -w3 -z {server_addr} 22")
# Check that unbanning works
output = server.succeed("reaction flush")
print(output)
client.succeed(f"nc -w3 -z {server_addr} 22")
'';
# Debug interactively with:
# - nix run .#nixosTests.reaction.driverInteractive -L
# - run_tests()
# ssh -o User=root vsock%3 (can also do vsock/3, but % works with scp etc.)
# ssh -o User=root vsock%4
interactive.sshBackdoor.enable = true;
interactive.nodes.server =
{ config, ... }:
{
# not needed, only for manual interactive debugging
virtualisation.memorySize = 4096;
virtualisation.graphics = false;
environment.systemPackages = with pkgs; [
btop
sysz
sshpass
libressl.nc
];
};
meta.maintainers =
with lib.maintainers;
[
ppom
]
++ lib.teams.ngi.members;
}

View file

@ -0,0 +1,74 @@
{ config, ... }:
let
banFor = name: duration: {
ban = {
type = "ipset";
options = {
set = "reaction-${name}";
action = "add";
};
};
unban = {
after = duration;
type = "ipset";
options = {
set = "reaction-${name}";
action = "del";
};
};
};
journalctl = "${config.systemd.package}/bin/journalctl";
in
{
patterns = {
ip = {
type = "ip";
ipv6mask = 64;
ignore = [
"127.0.0.1"
"::1"
];
ignorecidr = [
"10.1.1.0/24"
"2a01:e0a:b3a:1dd0::/64"
];
};
};
streams = {
ssh = {
cmd = [
journalctl
"-fn0"
"-o"
"cat"
"-u"
"sshd.service"
];
filters = {
failedlogin = {
regex = [
"authentication failure;.*rhost=<ip>(?: |$)"
"Failed password for .* from <ip> port"
"Invalid user .* from <ip> "
"Connection (?:reset|closed) by invalid user .* <ip> port"
];
retry = 2;
retryperiod = "6h";
actions = banFor "ssh" "48h";
};
connectionreset = {
regex = [
"Connection (?:reset|closed) by(?: authenticating user .*)? <ip> port"
"Received disconnect from <ip> port .*[preauth]"
"Timeout before authentication for connection from <ip> to"
];
retry = 2;
retryperiod = "6h";
actions = banFor "sshreset" "48h";
};
};
};
};
}