add restricted network nixos modules
See README.md for explanation
This commit is contained in:
@@ -49,6 +49,8 @@ in
|
||||
zt-tcp-relay = self.clanLib.test.containerTest ./zt-tcp-relay nixosTestArgs;
|
||||
matrix-synapse = self.clanLib.test.containerTest ./matrix-synapse nixosTestArgs;
|
||||
postgresql = self.clanLib.test.containerTest ./postgresql nixosTestArgs;
|
||||
user-firewall-iptables = self.clanLib.test.containerTest ./user-firewall/iptables.nix nixosTestArgs;
|
||||
user-firewall-nftables = self.clanLib.test.containerTest ./user-firewall/nftables.nix nixosTestArgs;
|
||||
|
||||
dummy-inventory-test = import ./dummy-inventory-test nixosTestArgs;
|
||||
dummy-inventory-test-from-flake = import ./dummy-inventory-test-from-flake nixosTestArgs;
|
||||
|
||||
100
checks/user-firewall/common.nix
Normal file
100
checks/user-firewall/common.nix
Normal file
@@ -0,0 +1,100 @@
|
||||
# Shared configuration for user firewall tests
|
||||
{ self, pkgs, ... }:
|
||||
{
|
||||
imports = [
|
||||
self.nixosModules.user-firewall
|
||||
];
|
||||
|
||||
networking.firewall.enable = true;
|
||||
|
||||
# Configure the user firewall module
|
||||
# Test with default allowedInterfaces (which includes wg*)
|
||||
networking.user-firewall = {
|
||||
# Use defaults for allowedInterfaces to test that wg* is included by default
|
||||
exemptUsers = [
|
||||
"root"
|
||||
"alice"
|
||||
];
|
||||
};
|
||||
|
||||
# Create test users
|
||||
users.users = {
|
||||
alice = {
|
||||
isNormalUser = true;
|
||||
uid = 1001;
|
||||
initialPassword = "test";
|
||||
};
|
||||
|
||||
bob = {
|
||||
isNormalUser = true;
|
||||
uid = 1002;
|
||||
initialPassword = "test";
|
||||
};
|
||||
};
|
||||
|
||||
# Add tools for testing
|
||||
environment.systemPackages = with pkgs; [
|
||||
curl
|
||||
netcat
|
||||
iproute2
|
||||
];
|
||||
|
||||
# Add a local web server for testing
|
||||
services.nginx = {
|
||||
enable = true;
|
||||
virtualHosts = {
|
||||
"localhost" = {
|
||||
listen = [
|
||||
{
|
||||
addr = "127.0.0.1";
|
||||
port = 8080;
|
||||
}
|
||||
];
|
||||
locations."/" = {
|
||||
return = "200 'test server response'";
|
||||
extraConfig = "add_header Content-Type text/plain;";
|
||||
};
|
||||
};
|
||||
"wg0-test" = {
|
||||
listen = [
|
||||
{
|
||||
addr = "10.100.0.2";
|
||||
port = 8081;
|
||||
}
|
||||
{
|
||||
addr = "[fd00::2]";
|
||||
port = 8081;
|
||||
}
|
||||
];
|
||||
locations."/" = {
|
||||
return = "200 'wg0 interface test response'";
|
||||
extraConfig = "add_header Content-Type text/plain;";
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
# Create a dummy interface to test allowed interface patterns
|
||||
systemd.services.setup-wg0-interface = {
|
||||
description = "Setup wg0 dummy interface";
|
||||
after = [ "network-pre.target" ];
|
||||
before = [ "network.target" ];
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
RemainAfterExit = true;
|
||||
};
|
||||
script = ''
|
||||
${pkgs.iproute2}/bin/ip link add wg0 type dummy || true
|
||||
${pkgs.iproute2}/bin/ip addr add 10.100.0.2/24 dev wg0 || true
|
||||
${pkgs.iproute2}/bin/ip addr add fd00::2/64 dev wg0 || true
|
||||
${pkgs.iproute2}/bin/ip link set wg0 up || true
|
||||
'';
|
||||
};
|
||||
|
||||
# Make nginx wait for the wg0 interface
|
||||
systemd.services.nginx = {
|
||||
after = [ "setup-wg0-interface.service" ];
|
||||
requires = [ "setup-wg0-interface.service" ];
|
||||
};
|
||||
}
|
||||
82
checks/user-firewall/iptables.nix
Normal file
82
checks/user-firewall/iptables.nix
Normal file
@@ -0,0 +1,82 @@
|
||||
{
|
||||
name = "user-firewall-iptables";
|
||||
|
||||
nodes = {
|
||||
router =
|
||||
{ ... }:
|
||||
{
|
||||
imports = [ ./router.nix ];
|
||||
};
|
||||
|
||||
machine =
|
||||
{ ... }:
|
||||
{
|
||||
imports = [ ./common.nix ];
|
||||
|
||||
# Force iptables backend
|
||||
networking.nftables.enable = false;
|
||||
};
|
||||
};
|
||||
|
||||
testScript = ''
|
||||
start_all()
|
||||
router.wait_for_unit("multi-user.target")
|
||||
router.wait_for_unit("nginx.service")
|
||||
machine.wait_for_unit("multi-user.target")
|
||||
machine.wait_for_unit("nginx.service")
|
||||
|
||||
# Get router IPs (both IPv4 and IPv6)
|
||||
router_ip = router.succeed("ip -4 addr show eth1 | grep -oP '(?<=inet\\s)\\d+(\\.\\d+){3}'").strip()
|
||||
router_ip6 = router.succeed("ip -6 addr show eth1 | grep -oP '(?<=inet6\\s)[0-9a-f:]+' | grep -v '^fe80' | head -1").strip()
|
||||
print(f"Router IPv4: {router_ip}")
|
||||
print(f"Router IPv6: {router_ip6}")
|
||||
|
||||
# Test firewall restart
|
||||
machine.succeed("systemctl restart firewall")
|
||||
machine.wait_for_unit("firewall.service")
|
||||
|
||||
# Verify rules are loaded
|
||||
machine.succeed("iptables -L user-firewall-output >&2")
|
||||
|
||||
# Test alice (exempt user) - should succeed both locally and to router
|
||||
machine.wait_until_succeeds("runuser -u alice -- curl -s http://127.0.0.1:8080")
|
||||
machine.succeed(f"runuser -u alice -- curl -s http://{router_ip}")
|
||||
machine.succeed(f"runuser -u alice -- curl -s http://[{router_ip6}]")
|
||||
|
||||
# Test bob (restricted user) - localhost should work, external should fail
|
||||
machine.succeed("runuser -u bob -- curl -s http://127.0.0.1:8080")
|
||||
# This should be blocked by firewall - IPv4
|
||||
result = machine.succeed(f"runuser -u bob -- curl -s --connect-timeout 2 http://{router_ip} 2>&1 || echo 'EXIT_CODE='$?")
|
||||
assert "EXIT_CODE=7" in result, f"Bob should be blocked from external IPv4 access (expected EXIT_CODE=7) but got: {result}"
|
||||
# This should be blocked by firewall - IPv6
|
||||
result6 = machine.succeed(f"runuser -u bob -- curl -s --connect-timeout 2 http://[{router_ip6}] 2>&1 || echo 'EXIT_CODE='$?")
|
||||
assert "EXIT_CODE=7" in result6, f"Bob should be blocked from external IPv6 access (expected EXIT_CODE=7) but got: {result6}"
|
||||
|
||||
# Verify the rules are actually present for both IPv4 and IPv6
|
||||
rules4 = machine.succeed("iptables -L user-firewall-output -n -v")
|
||||
assert "REJECT" in rules4, "REJECT rule not found in iptables"
|
||||
rules6 = machine.succeed("ip6tables -L user-firewall-output -n -v")
|
||||
assert "REJECT" in rules6, "REJECT rule not found in ip6tables"
|
||||
|
||||
# Wait for the dummy interface to be created
|
||||
machine.wait_for_unit("setup-wg0-interface.service")
|
||||
machine.wait_for_unit("nginx.service")
|
||||
machine.wait_for_open_port(8081, "10.100.0.2")
|
||||
|
||||
# Check that wg0 interface exists
|
||||
machine.succeed("ip link show wg0")
|
||||
machine.succeed("ip addr show wg0")
|
||||
|
||||
# The key test: users should be able to connect via wg0 interface
|
||||
# For alice (exempt user) - should work
|
||||
machine.succeed("runuser -u alice -- curl -s --interface wg0 http://10.100.0.2:8081/")
|
||||
machine.succeed("runuser -u alice -- curl -s --interface wg0 http://[fd00::2]:8081/") # IPv6 test
|
||||
|
||||
# For bob (restricted user) - should also work because wg* is in default allowedInterfaces
|
||||
machine.succeed("runuser -u bob -- curl -s --interface wg0 http://10.100.0.2:8081/")
|
||||
machine.succeed("runuser -u bob -- curl -s --interface wg0 http://[fd00::2]:8081/") # IPv6 test
|
||||
|
||||
# Verify that wg* interfaces are allowed in the firewall rules
|
||||
machine.succeed("iptables -L user-firewall-output -n -v | grep -E 'wg0|wg\\+' >&2")
|
||||
'';
|
||||
}
|
||||
77
checks/user-firewall/nftables.nix
Normal file
77
checks/user-firewall/nftables.nix
Normal file
@@ -0,0 +1,77 @@
|
||||
{
|
||||
name = "user-firewall-nftables";
|
||||
|
||||
nodes = {
|
||||
router = {
|
||||
imports = [ ./router.nix ];
|
||||
};
|
||||
|
||||
machine = {
|
||||
imports = [ ./common.nix ];
|
||||
|
||||
# Force nftables backend
|
||||
networking.nftables.enable = true;
|
||||
};
|
||||
};
|
||||
|
||||
testScript = ''
|
||||
start_all()
|
||||
router.wait_for_unit("multi-user.target")
|
||||
router.wait_for_unit("nginx.service")
|
||||
machine.wait_for_unit("multi-user.target")
|
||||
machine.wait_for_unit("nginx.service")
|
||||
|
||||
# Get router IPs (both IPv4 and IPv6)
|
||||
router_ip = router.succeed("ip -4 addr show eth1 | grep -oP '(?<=inet\\s)\\d+(\\.\\d+){3}'").strip()
|
||||
router_ip6 = router.succeed("ip -6 addr show eth1 | grep -oP '(?<=inet6\\s)[0-9a-f:]+' | grep -v '^fe80' | head -1").strip()
|
||||
print(f"Router IPv4: {router_ip}")
|
||||
print(f"Router IPv6: {router_ip6}")
|
||||
|
||||
# Test nftables restart
|
||||
machine.succeed("systemctl restart nftables")
|
||||
machine.wait_for_unit("nftables.service")
|
||||
|
||||
# Verify rules are loaded
|
||||
machine.succeed("nft list table inet user-firewall >&2")
|
||||
|
||||
# Test alice (exempt user) - should succeed both locally and to router
|
||||
machine.wait_until_succeeds("runuser -u alice -- curl -s http://127.0.0.1:8080")
|
||||
machine.succeed(f"runuser -u alice -- curl -s http://{router_ip}")
|
||||
machine.succeed(f"runuser -u alice -- curl -s http://[{router_ip6}]")
|
||||
|
||||
# Test bob (restricted user) - localhost should work, external should fail
|
||||
machine.succeed("runuser -u bob -- curl -s http://127.0.0.1:8080")
|
||||
# This should be blocked by firewall - IPv4
|
||||
result = machine.succeed(f"runuser -u bob -- curl -s --connect-timeout 2 http://{router_ip} 2>&1 || echo 'EXIT_CODE='$?")
|
||||
assert "EXIT_CODE=7" in result, f"Bob should be blocked from external IPv4 access (expected EXIT_CODE=7) but got: {result}"
|
||||
# This should be blocked by firewall - IPv6
|
||||
result6 = machine.succeed(f"runuser -u bob -- curl -s --connect-timeout 2 http://[{router_ip6}] 2>&1 || echo 'EXIT_CODE='$?")
|
||||
assert "EXIT_CODE=7" in result6, f"Bob should be blocked from external IPv6 access (expected EXIT_CODE=7) but got: {result6}"
|
||||
|
||||
# Verify the rules are actually present
|
||||
rules = machine.succeed("nft list table inet user-firewall")
|
||||
assert 'meta skuid 1002' in rules and 'reject' in rules, f"Reject rule for bob (uid 1002) not found in nftables. Actual rules:\n{rules}"
|
||||
assert "oifname" in rules, f"Interface rules not found in nftables. Actual rules:\n{rules}"
|
||||
|
||||
# Wait for the dummy interface to be created
|
||||
machine.wait_for_unit("setup-wg0-interface.service")
|
||||
machine.wait_for_unit("nginx.service")
|
||||
machine.wait_for_open_port(8081, "10.100.0.2")
|
||||
|
||||
# Check that wg0 interface exists
|
||||
machine.succeed("ip link show wg0")
|
||||
machine.succeed("ip addr show wg0")
|
||||
|
||||
# The key test: users should be able to connect via wg0 interface
|
||||
# For alice (exempt user) - should work
|
||||
machine.succeed("runuser -u alice -- curl -s --interface wg0 http://10.100.0.2:8081/")
|
||||
machine.succeed("runuser -u alice -- curl -s --interface wg0 http://[fd00::2]:8081/") # IPv6 test
|
||||
|
||||
# For bob (restricted user) - should also work because wg* is in default allowedInterfaces
|
||||
machine.succeed("runuser -u bob -- curl -s --interface wg0 http://10.100.0.2:8081/")
|
||||
machine.succeed("runuser -u bob -- curl -s --interface wg0 http://[fd00::2]:8081/") # IPv6 test
|
||||
|
||||
# Verify that wg* interfaces are allowed in the nftables rules
|
||||
rules_with_wg = machine.succeed("nft list table inet user-firewall | grep -E 'oifname.*wg' >&2")
|
||||
'';
|
||||
}
|
||||
32
checks/user-firewall/router.nix
Normal file
32
checks/user-firewall/router.nix
Normal file
@@ -0,0 +1,32 @@
|
||||
# Shared router configuration for user firewall tests
|
||||
{ ... }:
|
||||
{
|
||||
networking.firewall.enable = false;
|
||||
networking.useNetworkd = true;
|
||||
|
||||
# Simple web server to test connectivity
|
||||
services.nginx = {
|
||||
enable = true;
|
||||
virtualHosts."router" = {
|
||||
listen = [
|
||||
{
|
||||
addr = "0.0.0.0";
|
||||
port = 80;
|
||||
}
|
||||
{
|
||||
addr = "[::]";
|
||||
port = 80;
|
||||
}
|
||||
{
|
||||
addr = "10.100.0.1";
|
||||
port = 80;
|
||||
}
|
||||
];
|
||||
locations."/" = {
|
||||
return = "200 'router response'";
|
||||
extraConfig = "add_header Content-Type text/plain;";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user