mob next [ci-skip] [ci skip] [skip ci]
lastFile:docs/src/gateways.md
This commit is contained in:
@@ -0,0 +1,209 @@
|
||||
# End-to-end VM test of the router service: a PPPoE access concentrator plays
|
||||
# the ISP on the WAN port, a trunk carries tagged lan/iot VLANs to `client`,
|
||||
# and an untagged access port carries mgmt to `admin`.
|
||||
#
|
||||
# isp ---(vlan 1: PPPoE)--- wan [gw] trunk ---(vlan 2: tagged 20/40)--- client
|
||||
# access --(vlan 3: untagged mgmt)--- admin
|
||||
#
|
||||
# What is proven: PPPoE dial-in with the vars-provided credentials, bridge
|
||||
# VLAN tagging/untagging, Kea leases and reservations per VLAN, Blocky
|
||||
# answering on the VLAN with the blocklist active, NAT to the WAN, and the
|
||||
# firewall trust model (allowWan, mgmt-only SSH, no inter-VLAN forwarding).
|
||||
{ pkgs, lib, ... }:
|
||||
let
|
||||
# The vars mock answers every prompt with "mock-prompt-value-<name>"; the
|
||||
# ISP side must accept exactly those.
|
||||
chapSecrets = ''"mock-prompt-value-username" * "mock-prompt-value-password" *'';
|
||||
ispAddress = "192.0.2.1";
|
||||
|
||||
clientMac = "02:00:00:00:00:20";
|
||||
clientAddress = "10.9.20.50";
|
||||
adminMac = "02:00:00:00:00:10";
|
||||
adminAddress = "10.9.10.50";
|
||||
in
|
||||
{
|
||||
name = "router";
|
||||
|
||||
clan = {
|
||||
directory = ./.;
|
||||
# Bridges, VLAN netdevs, PPPoE and nftables need a real kernel.
|
||||
test.useContainers = false;
|
||||
inventory = {
|
||||
# Every node is a clan machine (the test framework's defaults require
|
||||
# it); only gw gets the router role.
|
||||
machines = {
|
||||
gw = { };
|
||||
isp = { };
|
||||
client = { };
|
||||
admin = { };
|
||||
};
|
||||
|
||||
instances.router = {
|
||||
module.name = "router";
|
||||
module.input = "self";
|
||||
roles.default.machines.gw.settings = {
|
||||
site = "tst";
|
||||
siteId = 9;
|
||||
mesh.subnet = "fd00:7e57:c1a1:c0de::/64";
|
||||
wan.interface = "wan";
|
||||
trunkPorts = [ "trunk" ];
|
||||
accessPorts.access = "mgmt";
|
||||
vlans = {
|
||||
mgmt = {
|
||||
id = 10;
|
||||
dhcp.reservations.admin = {
|
||||
hwAddress = adminMac;
|
||||
ipAddress = adminAddress;
|
||||
};
|
||||
};
|
||||
lan = {
|
||||
id = 20;
|
||||
dhcp.reservations.client = {
|
||||
hwAddress = clientMac;
|
||||
ipAddress = clientAddress;
|
||||
};
|
||||
};
|
||||
iot = {
|
||||
id = 40;
|
||||
allowWan = false;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
nodes = {
|
||||
gw = {
|
||||
virtualisation.interfaces = {
|
||||
wan = {
|
||||
vlan = 1;
|
||||
assignIP = false;
|
||||
};
|
||||
trunk = {
|
||||
vlan = 2;
|
||||
assignIP = false;
|
||||
};
|
||||
access = {
|
||||
vlan = 3;
|
||||
assignIP = false;
|
||||
};
|
||||
};
|
||||
|
||||
# Something must listen on 22 for the mgmt-only SSH rule to be observable
|
||||
# (a refused and a dropped connection look the same to the client).
|
||||
services.openssh.enable = true;
|
||||
|
||||
# The sandbox has no internet: serve the blocklist from a local file
|
||||
# instead of GitHub, and skip CrowdSec, whose hub sync needs the network
|
||||
# (it is not what this test exercises).
|
||||
services.blocky.settings.blocking.denylists.ads = lib.mkForce [
|
||||
(toString (pkgs.writeText "ads.hosts" "0.0.0.0 ads.example.com\n"))
|
||||
];
|
||||
services.crowdsec.enable = lib.mkForce false;
|
||||
services.crowdsec-firewall-bouncer.enable = lib.mkForce false;
|
||||
};
|
||||
|
||||
isp = {
|
||||
virtualisation.interfaces.wan = {
|
||||
vlan = 1;
|
||||
assignIP = false;
|
||||
};
|
||||
# PPPoE access concentrator: one session, peer gets 192.0.2.10.
|
||||
systemd.services.pppoe-server = {
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
after = [ "network.target" ];
|
||||
serviceConfig.ExecStart =
|
||||
"${pkgs.rp-pppoe}/sbin/pppoe-server -F -O /etc/ppp/pppoe-server-options"
|
||||
+ " -q ${pkgs.ppp}/sbin/pppd -I wan -L ${ispAddress} -R 192.0.2.10";
|
||||
};
|
||||
environment.etc = {
|
||||
"ppp/pppoe-server-options".text = ''
|
||||
plugin pppoe.so
|
||||
require-chap
|
||||
lcp-echo-interval 10
|
||||
lcp-echo-failure 2
|
||||
nobsdcomp
|
||||
noccp
|
||||
novj
|
||||
'';
|
||||
"ppp/chap-secrets" = {
|
||||
text = chapSecrets;
|
||||
mode = "0640";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
client = {
|
||||
virtualisation.interfaces.trunk = {
|
||||
vlan = 2;
|
||||
assignIP = false;
|
||||
};
|
||||
networking.useDHCP = false;
|
||||
networking.vlans = {
|
||||
lan0 = {
|
||||
id = 20;
|
||||
interface = "trunk";
|
||||
};
|
||||
iot0 = {
|
||||
id = 40;
|
||||
interface = "trunk";
|
||||
};
|
||||
};
|
||||
networking.interfaces.lan0 = {
|
||||
useDHCP = true;
|
||||
macAddress = clientMac;
|
||||
};
|
||||
networking.interfaces.iot0.useDHCP = true;
|
||||
environment.systemPackages = [
|
||||
pkgs.dnsutils
|
||||
pkgs.netcat
|
||||
];
|
||||
};
|
||||
|
||||
admin = {
|
||||
virtualisation.interfaces.access = {
|
||||
vlan = 3;
|
||||
assignIP = false;
|
||||
};
|
||||
networking.useDHCP = false;
|
||||
networking.interfaces.access = {
|
||||
useDHCP = true;
|
||||
macAddress = adminMac;
|
||||
};
|
||||
environment.systemPackages = [ pkgs.netcat ];
|
||||
};
|
||||
};
|
||||
|
||||
testScript = ''
|
||||
start_all()
|
||||
|
||||
with subtest("PPPoE session comes up with the vars credentials"):
|
||||
gw.wait_for_unit("pppd-wan.service")
|
||||
gw.wait_until_succeeds("ping -c1 -W1 ${ispAddress}")
|
||||
|
||||
with subtest("DHCP hands out reserved leases per VLAN"):
|
||||
gw.wait_for_unit("kea-dhcp4-server.service")
|
||||
client.wait_until_succeeds("ip -4 addr show lan0 | grep -q 'inet ${clientAddress}/24'")
|
||||
client.wait_until_succeeds("ip -4 addr show iot0 | grep -q 'inet 10.9.40.1[0-9][0-9]/24'")
|
||||
admin.wait_until_succeeds("ip -4 addr show access | grep -q 'inet ${adminAddress}/24'")
|
||||
|
||||
with subtest("Blocky serves the VLAN and blocks the denylist"):
|
||||
gw.wait_for_unit("blocky.service")
|
||||
answer = client.wait_until_succeeds("dig +short +time=2 @10.9.20.1 ads.example.com")
|
||||
assert answer.strip() == "0.0.0.0", f"expected blocked answer, got: {answer!r}"
|
||||
|
||||
with subtest("NAT to the WAN only for VLANs with allowWan"):
|
||||
client.wait_until_succeeds("ping -c1 -W1 -I lan0 ${ispAddress}")
|
||||
client.fail("ping -c1 -W2 -I iot0 ${ispAddress}")
|
||||
|
||||
with subtest("mgmt reaches other VLANs, other VLANs do not"):
|
||||
admin.succeed("ping -c1 -W2 ${clientAddress}")
|
||||
client.fail("ping -c1 -W2 -I lan0 ${adminAddress}")
|
||||
|
||||
with subtest("SSH on the router only from mgmt"):
|
||||
gw.wait_for_open_port(22)
|
||||
admin.succeed("nc -z -w2 10.9.10.1 22")
|
||||
client.fail("nc -z -w2 10.9.20.1 22")
|
||||
'';
|
||||
}
|
||||
Reference in New Issue
Block a user