Add router-on-a-stick, commit-confirm rollback and an example config
- Allow the WAN link to also carry LANs when the uplink is PPPoE on a VLAN with IPv6 off; only the PPP device counts as an uplink then, so LAN traffic can never match WAN rules. - Add router.rollback: router-arm / router-confirm / router-rollback plus `nix run .#deploy`, a commit-confirm deployment flow. - Wait for the LAN devices instead of the uplink in networkd wait-online, so an unplugged uplink cannot block boot or activation. - Add examples/desktop (single NIC, PPPoE on VLAN 1001) as nixosConfigurations.example, built as a flake check. Covered by the onestick and rollback VM tests and new assertion cases. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -5,11 +5,15 @@ let
|
||||
pppoe = cfg.wan.ipv4.type == "pppoe";
|
||||
vlan = cfg.wan.ipv4.pppoe.vlan;
|
||||
vlanDevice = "${cfg.wan.interface}.${toString vlan}";
|
||||
wanVlanOn = parent: lib.optional (pppoe && vlan != null && parent == cfg.wan.interface) vlanDevice;
|
||||
|
||||
lans = lib.attrValues cfg.lans;
|
||||
vlanLans = lib.filter (l: l.vlan != null) lans;
|
||||
# VLAN devices hang off their parent; the parent's own unit must list them.
|
||||
vlansOn = parent: map (l: l.device) (lib.filter (l: l.interface == parent) vlanLans);
|
||||
vlansOn =
|
||||
parent: map (l: l.device) (lib.filter (l: l.interface == parent) vlanLans) ++ wanVlanOn parent;
|
||||
# The WAN link's own unit is only needed when no LAN shares the link.
|
||||
sharedLink = lib.elem cfg.wan.interface (map (l: l.interface) lans);
|
||||
untaggedParents = map (l: l.interface) (lib.filter (l: l.vlan == null) lans);
|
||||
# Parents that carry only tagged LANs still need a unit to be brought up.
|
||||
trunkParents = lib.filter (p: !(lib.elem p untaggedParents)) (
|
||||
@@ -22,6 +26,12 @@ in
|
||||
config = lib.mkIf cfg.enable {
|
||||
networking.useNetworkd = true;
|
||||
networking.useDHCP = false;
|
||||
# Wait for the LAN devices (VLANs included: Kea only binds interfaces that
|
||||
# exist when it starts) but not for the uplink: an ISP outage or a pulled
|
||||
# cable must not hold up boot or a deployment.
|
||||
systemd.network.wait-online.extraArgs = map (l: "--interface=${l.device}") (
|
||||
lib.attrValues cfg.lans
|
||||
);
|
||||
|
||||
boot.kernel.sysctl = {
|
||||
"net.ipv4.conf.all.forwarding" = 1;
|
||||
@@ -49,87 +59,88 @@ in
|
||||
}
|
||||
) (lib.filterAttrs (_: l: l.vlan != null) cfg.lans);
|
||||
|
||||
systemd.network.networks = {
|
||||
"10-wan" = {
|
||||
matchConfig.Name = cfg.wan.interface;
|
||||
networkConfig = {
|
||||
DHCP = dhcp;
|
||||
systemd.network.networks =
|
||||
lib.optionalAttrs (!sharedLink) {
|
||||
"10-wan" = {
|
||||
matchConfig.Name = cfg.wan.interface;
|
||||
networkConfig = {
|
||||
DHCP = dhcp;
|
||||
}
|
||||
// lib.optionalAttrs v6 {
|
||||
IPv6AcceptRA = true;
|
||||
KeepConfiguration = "yes";
|
||||
}
|
||||
// lib.optionalAttrs (pppoe && vlan != null) {
|
||||
VLAN = [ vlanDevice ];
|
||||
};
|
||||
}
|
||||
// lib.optionalAttrs (!pppoe) {
|
||||
# The router runs its own resolver; never adopt the ISP's DNS for the host.
|
||||
dhcpV4Config.UseDNS = false;
|
||||
}
|
||||
// lib.optionalAttrs v6 {
|
||||
IPv6AcceptRA = true;
|
||||
KeepConfiguration = "yes";
|
||||
}
|
||||
// lib.optionalAttrs (pppoe && vlan != null) {
|
||||
VLAN = [ vlanDevice ];
|
||||
# The ISP sends RAs (default route) without DHCPv6 flags, so solicit anyway.
|
||||
dhcpV6Config = {
|
||||
WithoutRA = "solicit";
|
||||
UseDelegatedPrefix = true;
|
||||
UseDNS = false;
|
||||
};
|
||||
};
|
||||
}
|
||||
// lib.optionalAttrs (!pppoe) {
|
||||
# The router runs its own resolver; never adopt the ISP's DNS for the host.
|
||||
dhcpV4Config.UseDNS = false;
|
||||
}
|
||||
// lib.optionalAttrs v6 {
|
||||
# The ISP sends RAs (default route) without DHCPv6 flags, so solicit anyway.
|
||||
dhcpV6Config = {
|
||||
WithoutRA = "solicit";
|
||||
UseDelegatedPrefix = true;
|
||||
UseDNS = false;
|
||||
};
|
||||
};
|
||||
}
|
||||
// lib.optionalAttrs (pppoe && vlan != null) {
|
||||
# pppd configures the session; networkd only has to bring the device up.
|
||||
"11-wan-pppoe" = {
|
||||
matchConfig.Name = vlanDevice;
|
||||
networkConfig = {
|
||||
LinkLocalAddressing = "no";
|
||||
IPv6AcceptRA = false;
|
||||
};
|
||||
};
|
||||
}
|
||||
// lib.mapAttrs' (
|
||||
name: lan:
|
||||
lib.nameValuePair "20-lan-${name}" (
|
||||
{
|
||||
matchConfig.Name = lan.device;
|
||||
address = [ "${lan.ipv4.address}/${toString lan.ipv4.prefixLength}" ];
|
||||
networkConfig = {
|
||||
ConfigureWithoutCarrier = true;
|
||||
# Never accept RAs from the inside.
|
||||
IPv6AcceptRA = false;
|
||||
}
|
||||
// lib.optionalAttrs (lan.vlan == null && vlansOn lan.interface != [ ]) {
|
||||
VLAN = vlansOn lan.interface;
|
||||
}
|
||||
// lib.optionalAttrs lan.ipv6.pd {
|
||||
DHCPPrefixDelegation = true;
|
||||
IPv6SendRA = true;
|
||||
};
|
||||
}
|
||||
// lib.optionalAttrs lan.ipv6.pd {
|
||||
dhcpPrefixDelegationConfig = {
|
||||
UplinkInterface = cfg.wan.interface;
|
||||
SubnetId = 0;
|
||||
Announce = true;
|
||||
Assign = true;
|
||||
};
|
||||
# No RDNSS yet: the global address is unknown at evaluation time.
|
||||
ipv6SendRAConfig.EmitDNS = false;
|
||||
}
|
||||
)
|
||||
) cfg.lans
|
||||
// lib.listToAttrs (
|
||||
map (
|
||||
parent:
|
||||
lib.nameValuePair "19-trunk-${parent}" {
|
||||
matchConfig.Name = parent;
|
||||
// lib.optionalAttrs (pppoe && vlan != null) {
|
||||
# pppd configures the session; networkd only has to bring the device up.
|
||||
"11-wan-pppoe" = {
|
||||
matchConfig.Name = vlanDevice;
|
||||
networkConfig = {
|
||||
LinkLocalAddressing = "no";
|
||||
IPv6AcceptRA = false;
|
||||
ConfigureWithoutCarrier = true;
|
||||
VLAN = vlansOn parent;
|
||||
};
|
||||
}
|
||||
) trunkParents
|
||||
);
|
||||
};
|
||||
}
|
||||
// lib.mapAttrs' (
|
||||
name: lan:
|
||||
lib.nameValuePair "20-lan-${name}" (
|
||||
{
|
||||
matchConfig.Name = lan.device;
|
||||
address = [ "${lan.ipv4.address}/${toString lan.ipv4.prefixLength}" ];
|
||||
networkConfig = {
|
||||
ConfigureWithoutCarrier = true;
|
||||
# Never accept RAs from the inside.
|
||||
IPv6AcceptRA = false;
|
||||
}
|
||||
// lib.optionalAttrs (lan.vlan == null && vlansOn lan.interface != [ ]) {
|
||||
VLAN = vlansOn lan.interface;
|
||||
}
|
||||
// lib.optionalAttrs lan.ipv6.pd {
|
||||
DHCPPrefixDelegation = true;
|
||||
IPv6SendRA = true;
|
||||
};
|
||||
}
|
||||
// lib.optionalAttrs lan.ipv6.pd {
|
||||
dhcpPrefixDelegationConfig = {
|
||||
UplinkInterface = cfg.wan.interface;
|
||||
SubnetId = 0;
|
||||
Announce = true;
|
||||
Assign = true;
|
||||
};
|
||||
# No RDNSS yet: the global address is unknown at evaluation time.
|
||||
ipv6SendRAConfig.EmitDNS = false;
|
||||
}
|
||||
)
|
||||
) cfg.lans
|
||||
// lib.listToAttrs (
|
||||
map (
|
||||
parent:
|
||||
lib.nameValuePair "19-trunk-${parent}" {
|
||||
matchConfig.Name = parent;
|
||||
networkConfig = {
|
||||
LinkLocalAddressing = "no";
|
||||
IPv6AcceptRA = false;
|
||||
ConfigureWithoutCarrier = true;
|
||||
VLAN = vlansOn parent;
|
||||
};
|
||||
}
|
||||
) trunkParents
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user