Files
cnx-network-clan/modules/clan/router
..
2026-09-17 08:57:32 +07:00
2026-09-17 08:57:32 +07:00
2026-09-17 08:57:32 +07:00
2026-09-17 08:57:32 +07:00
2026-09-17 08:57:32 +07:00
2026-09-17 08:57:32 +07:00
2026-09-17 08:57:32 +07:00
2026-09-17 08:57:32 +07:00
2026-09-17 08:57:32 +07:00
2026-09-17 08:57:32 +07:00
2026-09-17 08:57:32 +07:00
2026-09-17 08:57:32 +07:00
2026-09-17 08:57:32 +07:00
2026-09-17 08:57:32 +07:00

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

# 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
    # stagingPort = "enp3s0";       # DHCP uplink into the old LAN until cutover
    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:

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:

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.