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).
91 lines
3.4 KiB
Markdown
91 lines
3.4 KiB
Markdown
# router
|
|
|
|
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: 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`
|
|
and `lan` VLANs are mandatory. Trust model: mgmt reaches everything; other
|
|
VLANs get router DNS/DHCP and (with `allowWan`) the internet, no inter-VLAN;
|
|
WAN nothing inbound; the admin mesh (`mesh.subnet`) gets SSH, metrics, iperf3
|
|
and the Omada UI.
|
|
|
|
## Usage from another clan
|
|
|
|
```nix
|
|
# flake.nix
|
|
inputs.cnx-network.url = "git+https://<host>/B4L/cnx-network-clan";
|
|
|
|
# clan.nix
|
|
inventory.instances.router = {
|
|
module = { name = "router"; input = "cnx-network"; };
|
|
roles.default.settings.mesh.subnet = "fd..::/88"; # your admin overlay
|
|
roles.default.machines.gw-1.settings = {
|
|
site = "ams";
|
|
siteId = 1;
|
|
wan.interface = "enp1s0";
|
|
wan.vlanId = 10; # or null for untagged PPPoE
|
|
trunkPorts = [ "enp2s0" ];
|
|
accessPorts.enp4s0 = "mgmt"; # untagged on-site recovery port
|
|
vlans = {
|
|
mgmt.id = 10;
|
|
lan.id = 20;
|
|
iot = { id = 40; allowWan = false; };
|
|
};
|
|
};
|
|
};
|
|
```
|
|
|
|
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
|
|
certificate obtained via RFC 2136 DNS-01 against `proxy.acme.nameserver`. The
|
|
gateway signs updates with TSIG key `acme_<hostname with _>`, whose secret is
|
|
the shared `dns-acme-<hostname>-secret` generator declared by this service.
|
|
The nameserver machine must declare the same generator so both sides hold one
|
|
secret — import `acme-secret.nix` from this directory with the gateway's name:
|
|
|
|
```nix
|
|
imports = [ (import "${inputs.cnx-network}/modules/clan/router/acme-secret.nix" "gw-1") ];
|
|
```
|
|
|
|
and load the key with an acl scoped to `_acme-challenge.<site><siteId>`.
|
|
|
|
The service does not open the WAN to anything; reach gateways over your mesh.
|
|
One instance per machine.
|