{ ... }: { _class = "clan.service"; manifest.name = "gw-router"; manifest.description = "A gateway router service to configure most of a router features"; manifest.readme = "A gateway router service to configure most of a router features"; manifest.categories = [ "System" ]; roles.default = { description = "Site gateway router role"; interface = { lib, config, ... }: let vlanModule = { name, config, ... }: let octet = toString config.id; in { options = { id = lib.mkOption { type = lib.types.ints.between 1 4094; description = "802.1Q VLAN id."; }; address = lib.mkOption { type = lib.types.str; example = "10.0.10.1"; description = "Router address on this VLAN."; }; prefixLength = lib.mkOption { type = lib.types.ints.between 8 30; default = 24; }; subnet = lib.mkOption { type = lib.types.str; example = "10.0.10.0/24"; description = "The VLAN's network in CIDR form (must contain `address`)."; }; dhcp = { enable = lib.mkOption { type = lib.types.bool; default = true; }; pool = { from = lib.mkOption { type = lib.types.str; example = "10.0.10.100"; }; to = lib.mkOption { type = lib.types.str; example = "10.0.10.199"; }; }; leaseTime = lib.mkOption { type = lib.types.ints.positive; default = 86400; description = '' Lease validity in seconds. Lower it for high-churn networks, e.g. public-WiFi guest VLANs (3600-7200), so the pool recycles. ''; }; fixedIPs = lib.mkOption { type = lib.types.attrsOf ( lib.types.submodule { options = { hwAddress = lib.mkOption { type = lib.types.str; example = "aa:bb:cc:dd:ee:ff"; description = "Client MAC address."; }; ipAddress = lib.mkOption { type = lib.types.str; description = "Fixed address handed to this client (inside the VLAN's subnet, outside the pool)."; }; }; } ); default = { }; description = "Static DHCP leases; the attribute name becomes the client's hostname."; }; }; allowWAN = lib.mkOption { type = lib.types.bool; default = true; description = "Whether clients on this VLAN may reach the internet."; }; }; }; in { options = { wan = { interface = lib.mkOption { type = lib.types.str; description = "Physical WAN port the PPPoE session runs on."; }; vlanId = lib.mkOption { type = lib.types.nullOr (lib.types.ints.between 1 4094); default = null; description = '' 802.1Q tag the ISP requires for the PPPoE session (AIS Thailand: 10); null for untagged PPPoE directly on the port. Unrelated to the LAN VLANs — this tag exists only on the WAN port. ''; }; macAddress = lib.mkOption { type = lib.types.nullOr lib.types.str; default = null; example = "aa:bb:cc:dd:ee:ff"; description = '' Spoofed MAC for the WAN port, e.g. to keep the MAC the ISP has pinned (cloned from the old router). null keeps the hardware MAC. ''; }; pppInterface = lib.mkOption { type = lib.types.str; internal = true; readOnly = true; default = if config.wan.vlanId == null then config.wan.interface else "wan-vlan"; description = "Interface pppd dials on (the WAN port or its ISP VLAN)."; }; }; vlans = lib.mkOption { type = lib.types.attrsOf (lib.types.submodule vlanModule); description = "VLANs setup for this router"; }; trunkPorts = lib.mkOption { type = lib.types.listOf lib.types.str; description = "LAN ports carrying all VLANs tagged (incl. any 10G SFP+ ports)."; }; accessPorts = lib.mkOption { type = lib.types.listOf lib.types.str; description = '' Ports acting as untagged access ports on a single VLAN (port name -> VLAN name). Frames are untagged on the wire; the bridge tags them with the VLAN's PVID. Use for an always-available on-site mgmt port. ''; }; }; }; perInstance = { settings, ... }: { nixosModule = { config, pkgs, lib, ... }: let vlanIf = name: "vlan=${name}"; in { networking.useNetworkd = true; networking.useDHCP = false; systemd.network.enable = true; systemd.network.netdevs = { "20-br0" = { netdevConfig = { Name = "br0"; Kind = "bridge"; }; bridgeConfig = { VLANFiltering = true; }; }; } // lib.optionalAttrs (settings.wan.vlanId != null) { "15-wan-vlan" = { netdevConfig = { Name = "wan-vlan"; Kind = "vlan"; }; vlanConfig.Id = settings.wan.vlanId; }; } // lib.mapAttrs' ( name: vlan: lib.nameValuePair "30-${vlanIf name}" { netdevConfig = { Name = vlanIf name; Kind = "vlan"; }; vlanConfig.Id = vlan.id; } ) settings.vlans; systemd.network.networks = { "10-wan" = { matchConfig.Name = settings.wan.interface; networkConfig.LinkLocalAddressing = "no"; }; }; environment.systemPackages = with pkgs; [ tcpdump mtr ethtool conntrack-tools knot-dns iftop librespeed-cli ]; }; }; }; imports = [ ./pppoe.nix ]; }