171 lines
5.1 KiB
Nix
171 lines
5.1 KiB
Nix
# L2/L3 of the gateway: PPPoE WAN port (optionally on an ISP VLAN), a
|
|
# VLAN-filtering bridge over the LAN ports, and one L3 interface per VLAN.
|
|
{ settings }:
|
|
{ lib, pkgs, ... }:
|
|
let
|
|
cfg = settings;
|
|
|
|
vlanIf = name: "vlan-${name}";
|
|
in
|
|
{
|
|
assertions = [
|
|
{
|
|
assertion = cfg.vlans ? mgmt && cfg.vlans ? lan;
|
|
message = "router: every site must define the `mgmt` and `lan` VLANs.";
|
|
}
|
|
{
|
|
assertion = lib.all (v: cfg.vlans ? ${v}) (lib.attrValues cfg.accessPorts);
|
|
message = "router: every accessPorts value must name a defined VLAN.";
|
|
}
|
|
{
|
|
assertion = lib.all (p: !(cfg.accessPorts ? ${p})) cfg.trunkPorts;
|
|
message = "router: a port cannot be both a trunk and an access port.";
|
|
}
|
|
{
|
|
assertion =
|
|
cfg.stagingPort == null
|
|
|| !(
|
|
cfg.stagingPort == cfg.wan.interface
|
|
|| lib.elem cfg.stagingPort cfg.trunkPorts
|
|
|| cfg.accessPorts ? ${cfg.stagingPort}
|
|
);
|
|
message = "router: stagingPort ${toString cfg.stagingPort} is also the WAN, a trunk or an access port.";
|
|
}
|
|
];
|
|
|
|
# Router diagnostics toolkit: packets (tcpdump), path (mtr), link
|
|
# negotiation (ethtool), NAT state (conntrack), DNS (kdig), per-flow
|
|
# bandwidth (iftop), WAN throughput (librespeed-cli; iperf3 covers LAN).
|
|
environment.systemPackages = with pkgs; [
|
|
tcpdump
|
|
mtr
|
|
ethtool
|
|
conntrack-tools
|
|
knot-dns
|
|
iftop
|
|
librespeed-cli
|
|
];
|
|
|
|
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 (cfg.wan.vlanId != null) {
|
|
"15-wan-vlan" = {
|
|
netdevConfig = {
|
|
Name = "wan-vlan";
|
|
Kind = "vlan";
|
|
};
|
|
vlanConfig.Id = cfg.wan.vlanId;
|
|
};
|
|
}
|
|
// lib.mapAttrs' (
|
|
name: vlan:
|
|
lib.nameValuePair "30-${vlanIf name}" {
|
|
netdevConfig = {
|
|
Name = vlanIf name;
|
|
Kind = "vlan";
|
|
};
|
|
vlanConfig.Id = vlan.id;
|
|
}
|
|
) cfg.vlans;
|
|
|
|
systemd.network.networks =
|
|
let
|
|
taggedAll = lib.mapAttrsToList (_: vlan: { VLAN = vlan.id; }) cfg.vlans;
|
|
in
|
|
lib.optionalAttrs (cfg.stagingPort != null) {
|
|
# Staging uplink (see interface.nix): plain DHCPv4 client on a spare
|
|
# port, no bridge/VLAN membership, so the firewall treats it as untrusted.
|
|
"05-staging" = {
|
|
matchConfig.Name = cfg.stagingPort;
|
|
networkConfig.DHCP = "ipv4";
|
|
};
|
|
}
|
|
// {
|
|
# WAN port carries only the PPPoE session; no IP config of its own.
|
|
"10-wan" = {
|
|
matchConfig.Name = cfg.wan.interface;
|
|
networkConfig.LinkLocalAddressing = "no";
|
|
vlan = lib.optional (cfg.wan.vlanId != null) "wan-vlan";
|
|
linkConfig = {
|
|
RequiredForOnline = "carrier";
|
|
}
|
|
# The wan-vlan subinterface (and thus the PPPoE session) inherits
|
|
# the parent port's MAC, so spoofing here covers both cases.
|
|
// lib.optionalAttrs (cfg.wan.macAddress != null) {
|
|
MACAddress = cfg.wan.macAddress;
|
|
};
|
|
};
|
|
}
|
|
// lib.optionalAttrs (cfg.wan.vlanId != null) {
|
|
# The ISP-side VLAN subinterface pppd dials on (e.g. AIS tags PPPoE).
|
|
"15-wan-vlan" = {
|
|
matchConfig.Name = "wan-vlan";
|
|
networkConfig.LinkLocalAddressing = "no";
|
|
linkConfig.RequiredForOnline = "no";
|
|
};
|
|
}
|
|
// {
|
|
# The bridge itself is L2-only; L3 lives on the vlan-* interfaces,
|
|
# which hang off the bridge (tagged on the bridge "self" port).
|
|
"20-br0" = {
|
|
matchConfig.Name = "br0";
|
|
networkConfig.LinkLocalAddressing = "no";
|
|
vlan = lib.mapAttrsToList (name: _: vlanIf name) cfg.vlans;
|
|
bridgeVLANs = taggedAll;
|
|
linkConfig.RequiredForOnline = "no";
|
|
};
|
|
}
|
|
// lib.listToAttrs (
|
|
map (port: {
|
|
name = "25-trunk-${port}";
|
|
value = {
|
|
matchConfig.Name = port;
|
|
networkConfig.Bridge = "br0";
|
|
bridgeVLANs = taggedAll;
|
|
linkConfig.RequiredForOnline = "no";
|
|
};
|
|
}) cfg.trunkPorts
|
|
)
|
|
// lib.mapAttrs' (
|
|
port: vlanName:
|
|
lib.nameValuePair "25-access-${port}" {
|
|
matchConfig.Name = port;
|
|
networkConfig.Bridge = "br0";
|
|
bridgeVLANs = [
|
|
{
|
|
VLAN = cfg.vlans.${vlanName}.id;
|
|
PVID = cfg.vlans.${vlanName}.id;
|
|
EgressUntagged = cfg.vlans.${vlanName}.id;
|
|
}
|
|
];
|
|
linkConfig.RequiredForOnline = "no";
|
|
}
|
|
) cfg.accessPorts
|
|
// lib.mapAttrs' (
|
|
name: vlan:
|
|
lib.nameValuePair "40-${vlanIf name}" {
|
|
matchConfig.Name = vlanIf name;
|
|
address = [ "${vlan.address}/${toString vlan.prefixLength}" ];
|
|
networkConfig = {
|
|
IPv6AcceptRA = false;
|
|
# Announce a /64 carved from the DHCPv6-PD prefix on ppp0 (SLAAC).
|
|
IPv6SendRA = true;
|
|
DHCPPrefixDelegation = true;
|
|
};
|
|
dhcpPrefixDelegationConfig.SubnetId = "auto";
|
|
linkConfig.RequiredForOnline = "no";
|
|
}
|
|
) cfg.vlans;
|
|
}
|