IPv6: router.wan.ipv6.enable mirrors the existing uplink config (DHCPv6 with WithoutRA=solicit, delegated prefix). The ISP delegates a single /64, so exactly one LAN may claim it via router.lans.<name>.ipv6.pd (asserted); other LANs stay IPv4-only. SLAAC only, no NAT66. PPPoE: router.wan.ipv4.type = "pppoe" runs pppd on an optional VLAN of the uplink, with the password read from a file at service start (never the Nix store). NAT, forwarding and MSS clamping (MTU 1492) use the derived PPP device; native IPv6 stays on the untagged link. Tests: basic.nix is parameterised by uplink type; the pppoe variant uses a fake ISP with pppoe-server (VLAN 7, CHAP). Both variants cover IPv6 PD, SLAAC, routing without NAT and WAN-deny. Assertion tests gain PD/PPPoE cases and a full-system evaluation step. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
163 lines
5.0 KiB
Nix
163 lines
5.0 KiB
Nix
{ config, lib, ... }:
|
|
let
|
|
inherit (lib) mkOption mkEnableOption types;
|
|
rlib = import ./lib.nix { inherit lib; };
|
|
in
|
|
{
|
|
options.router = {
|
|
enable = mkEnableOption "the router role";
|
|
|
|
wan = {
|
|
interface = mkOption {
|
|
type = types.str;
|
|
example = "eth0";
|
|
description = ''
|
|
Physical uplink interface. Native IPv6 (DHCPv6) runs on it untagged;
|
|
with PPPoE it also carries the (optionally tagged) PPPoE session.
|
|
'';
|
|
};
|
|
|
|
ipv4 = {
|
|
type = mkOption {
|
|
type = types.enum [
|
|
"dhcp"
|
|
"pppoe"
|
|
];
|
|
default = "dhcp";
|
|
description = "How the router gets its IPv4 uplink.";
|
|
};
|
|
|
|
pppoe = {
|
|
vlan = mkOption {
|
|
type = types.nullOr (types.ints.between 1 4094);
|
|
default = null;
|
|
description = "VLAN ID the ISP requires for PPPoE, or null for untagged.";
|
|
};
|
|
username = mkOption {
|
|
type = types.str;
|
|
default = "";
|
|
description = "PPP authentication username.";
|
|
};
|
|
passwordFile = mkOption {
|
|
type = types.nullOr (
|
|
types.pathWith {
|
|
absolute = true;
|
|
inStore = false;
|
|
}
|
|
);
|
|
default = null;
|
|
example = "/var/lib/secrets/pppoe-password";
|
|
description = ''
|
|
File containing the PPP password, read at service start. Must not
|
|
be in the Nix store (it would be world-readable).
|
|
'';
|
|
};
|
|
mtu = mkOption {
|
|
type = types.ints.between 576 1500;
|
|
default = 1492;
|
|
description = "PPP MTU/MRU; TCP MSS is clamped to match on forwarded traffic.";
|
|
};
|
|
};
|
|
|
|
interface = mkOption {
|
|
type = types.str;
|
|
readOnly = true;
|
|
internal = true;
|
|
description = "Device IPv4 traffic leaves through (derived).";
|
|
};
|
|
};
|
|
|
|
ipv6.enable = mkEnableOption ''
|
|
IPv6 on the uplink: DHCPv6 (soliciting even without RA flags) plus
|
|
prefix delegation. The ISP is expected to delegate a single /64
|
|
'';
|
|
};
|
|
|
|
lans = mkOption {
|
|
default = { };
|
|
description = "LAN segments served by this router, keyed by name.";
|
|
type = types.attrsOf (
|
|
types.submodule (
|
|
{ config, ... }:
|
|
{
|
|
options = {
|
|
interface = mkOption {
|
|
type = types.str;
|
|
example = "eth1";
|
|
description = "Interface (or bridge/VLAN device) carrying this LAN.";
|
|
};
|
|
|
|
ipv4 = {
|
|
address = mkOption {
|
|
type = types.str;
|
|
example = "192.168.10.1";
|
|
description = "Router address on this LAN.";
|
|
};
|
|
prefixLength = mkOption {
|
|
type = types.ints.between 8 30;
|
|
default = 24;
|
|
};
|
|
subnet = mkOption {
|
|
type = types.str;
|
|
readOnly = true;
|
|
description = "Network in CIDR notation, derived from `address` and `prefixLength`.";
|
|
};
|
|
};
|
|
|
|
dhcp = {
|
|
enable = mkOption {
|
|
type = types.bool;
|
|
default = true;
|
|
};
|
|
pool = {
|
|
start = mkOption {
|
|
type = types.str;
|
|
example = "192.168.10.100";
|
|
};
|
|
end = mkOption {
|
|
type = types.str;
|
|
example = "192.168.10.199";
|
|
};
|
|
};
|
|
};
|
|
|
|
ipv6.pd = mkOption {
|
|
type = types.bool;
|
|
default = false;
|
|
description = ''
|
|
Whether this LAN receives the /64 delegated on the uplink
|
|
(assigned to the interface and announced via SLAAC). A /64
|
|
cannot be split, so at most one LAN may set this; the others
|
|
stay IPv4-only.
|
|
'';
|
|
};
|
|
|
|
allowManagement = mkOption {
|
|
type = types.bool;
|
|
default = true;
|
|
description = "Whether clients on this LAN may reach SSH on the router.";
|
|
};
|
|
};
|
|
|
|
# "invalid" is only ever seen when assertions.nix is already failing.
|
|
config.ipv4.subnet =
|
|
if rlib.ipv4.parse config.ipv4.address == null then
|
|
"invalid"
|
|
else
|
|
rlib.ipv4.cidr config.ipv4.address config.ipv4.prefixLength;
|
|
}
|
|
)
|
|
);
|
|
};
|
|
|
|
dns.forwarders = mkOption {
|
|
type = types.listOf types.str;
|
|
default = [ ];
|
|
description = "Upstream resolvers. When empty, Unbound resolves recursively.";
|
|
};
|
|
};
|
|
|
|
config.router.wan.ipv4.interface =
|
|
if config.router.wan.ipv4.type == "pppoe" then "pppoe-wan" else config.router.wan.interface;
|
|
}
|