router: optional Wi-Fi access point, enforce allowWan

wifi.* settings turn the gateway's own radios into the site AP (hostapd):
SSIDs are defined once in wifi.networks and act as untagged access ports of
their VLAN (each BSS joins br0 with the VLAN's PVID), radios pick what they
broadcast, passphrases are vars prompts. Extra SSIDs on a radio get BSSIDs
derived from its hardware address. A udev rule puts configured radios in AP
mode on appearance, since networkd cannot bridge a station-mode interface
and gives up before hostapd switches it. wifi.enable gates all of it.

The VM test grows a hwsim radio with two SSIDs and a WPA3 station in its
own netns that must get a Kea lease on the SSID's VLAN. Making the client
deterministic (route metrics, loose rp-filter, a guard against a vacuous
negative check) exposed that allowWan was never enforced: networking.nat
opens forward-to-WAN for all of its internalInterfaces, which listed every
VLAN. It now lists only the allowWan VLANs; the duplicate custom rule is
gone. No behavioural change on gw-cnx-1 (all its VLANs allow WAN).
This commit is contained in:
2026-09-09 09:24:34 +00:00
parent a3705e7a93
commit 63e8b6252c
7 changed files with 474 additions and 9 deletions
+31 -2
View File
@@ -4,8 +4,9 @@ Turns a machine with several NICs into a site gateway: PPPoE WAN (ISP
credentials via vars prompts), a VLAN-filtering bridge over the LAN ports with
one L3 interface per VLAN, Kea DHCP and Blocky DNS per VLAN, nftables
firewall/NAT, DHCPv6-PD, CrowdSec, an iperf3 server and a WAN speed-test
timer. Optional: the TP-Link Omada controller (podman) and an internal Caddy
reverse proxy with a real wildcard certificate (ACME DNS-01).
timer. Optional: a Wi-Fi access point on the router's own radios (hostapd),
the TP-Link Omada controller (podman) and an internal Caddy reverse proxy
with a real wildcard certificate (ACME DNS-01).
Addressing convention: a site owns `10.<siteId>.0.0/16`; VLAN `<id>` defaults
to `10.<siteId>.<id>.0/24`, router at `.1`, DHCP pool `.100-.199`. The `mgmt`
@@ -42,6 +43,34 @@ inventory.instances.router = {
Then `clan vars generate gw-1` prompts for the PPPoE username/password.
### Wi-Fi access point
If the box has wireless cards, the router can be the site's AP. An SSID is
defined once and behaves like an untagged access port of its VLAN; radios
choose what to broadcast, so a dual-band card serves the same SSID twice:
```nix
wifi = {
enable = true;
countryCode = "TH";
networks = {
home.vlan = "lan"; # WPA3 with WPA2 fallback
things = { vlan = "iot"; security = "wpa2"; }; # legacy IoT
guest = { vlan = "guest"; isolateClients = true; };
};
radios = {
wlp5s0 = { band = "2g"; channel = 6; macAddress = ""; networks = [ "home" "things" ]; };
wlp6s0 = { band = "5g"; channel = 36; networks = [ "home" ]; };
};
};
```
Passphrases are vars prompts (`wifi-<name>-passphrase`), asked once at `clan
vars generate`. A radio broadcasting more than one SSID needs its hardware
`macAddress`: hostapd wants a fixed BSSID per extra SSID, derived from it.
`security = "wpa3-transition"` (the default) offers SAE and WPA2-PSK-SHA256;
devices that only speak classic WPA2-PSK need `security = "wpa2"`.
### Internal proxy
`proxy.enable` serves `<name>.<site><siteId>.<proxy.domain>` under a wildcard