diff --git a/nixos/lib/systemd-lib.nix b/nixos/lib/systemd-lib.nix index b67495609ff5..198a710f052d 100644 --- a/nixos/lib/systemd-lib.nix +++ b/nixos/lib/systemd-lib.nix @@ -148,6 +148,10 @@ in rec { optional (attr ? ${name} && !(min <= attr.${name} && max >= attr.${name})) "Systemd ${group} field `${name}' is outside the range [${toString min},${toString max}]"; + assertRangeOrOneOf = name: min: max: values: group: attr: + optional (attr ? ${name} && !((min <= attr.${name} && max >= attr.${name}) || elem attr.${name} values)) + "Systemd ${group} field `${name}' is not a value in range [${toString min},${toString max}], or one of ${toString values}"; + assertMinimum = name: min: group: attr: optional (attr ? ${name} && attr.${name} < min) "Systemd ${group} field `${name}' must be greater than or equal to ${toString min}"; diff --git a/nixos/lib/systemd-network-units.nix b/nixos/lib/systemd-network-units.nix index 986586a61a70..ae581495772a 100644 --- a/nixos/lib/systemd-network-units.nix +++ b/nixos/lib/systemd-network-units.nix @@ -25,6 +25,9 @@ in { commonMatchText def + '' [NetDev] ${attrsToSection def.netdevConfig} + '' + optionalString (def.bridgeConfig != { }) '' + [Bridge] + ${attrsToSection def.bridgeConfig} '' + optionalString (def.vlanConfig != { }) '' [VLAN] ${attrsToSection def.vlanConfig} diff --git a/nixos/modules/system/boot/networkd.nix b/nixos/modules/system/boot/networkd.nix index 0ddb7dcb651d..bb899c8d8999 100644 --- a/nixos/modules/system/boot/networkd.nix +++ b/nixos/modules/system/boot/networkd.nix @@ -186,6 +186,37 @@ let (assertNetdevMacAddress "MACAddress") ]; + sectionBridge = checkUnitConfig "Bridge" [ + (assertOnlyFields [ + "HelloTimeSec" + "MaxAgeSec" + "ForwardDelaySec" + "AgeingTimeSec" + "Priority" + "GroupForwardMask" + "DefaultPVID" + "MulticastQuerier" + "MulticastSnooping" + "VLANFiltering" + "VLANProtocol" + "STP" + "MulticastIGMPVersion" + ]) + (assertInt "HelloTimeSec") + (assertInt "MaxAgeSec") + (assertInt "ForwardDelaySec") + (assertInt "AgeingTimeSec") + (assertRange "Priority" 0 65535) + (assertRange "GroupForwardMask" 0 65535) + (assertRangeOrOneOf "DefaultPVID" 0 4094 ["none"]) + (assertValueOneOf "MulticastQuerier" boolValues) + (assertValueOneOf "MulticastSnooping" boolValues) + (assertValueOneOf "VLANFiltering" boolValues) + (assertValueOneOf "VLANProtocol" ["802.1q" "802.ad"]) + (assertValueOneOf "STP" boolValues) + (assertValueOneOf "MulticastIGMPVersion" [2 3]) + ]; + sectionVLAN = checkUnitConfig "VLAN" [ (assertOnlyFields [ "Id" @@ -1635,6 +1666,17 @@ let ''; }; + bridgeConfig = mkOption { + default = {}; + example = { STP = true; }; + type = types.addCheck (types.attrsOf unitOption) check.netdev.sectionBridge; + description = '' + Each attribute in this set specifies an option in the + `[Bridge]` section of the unit. See + {manpage}`systemd.netdev(5)` for details. + ''; + }; + vlanConfig = mkOption { default = {}; example = { Id = 4; }; diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index 7944952e2f4e..bd496d09a52a 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -899,6 +899,7 @@ in { systemd-lock-handler = runTestOn ["aarch64-linux" "x86_64-linux"] ./systemd-lock-handler.nix; systemd-machinectl = handleTest ./systemd-machinectl.nix {}; systemd-networkd = handleTest ./systemd-networkd.nix {}; + systemd-networkd-bridge = handleTest ./systemd-networkd-bridge.nix {}; systemd-networkd-dhcpserver = handleTest ./systemd-networkd-dhcpserver.nix {}; systemd-networkd-dhcpserver-static-leases = handleTest ./systemd-networkd-dhcpserver-static-leases.nix {}; systemd-networkd-ipv6-prefix-delegation = handleTest ./systemd-networkd-ipv6-prefix-delegation.nix {}; diff --git a/nixos/tests/systemd-networkd-bridge.nix b/nixos/tests/systemd-networkd-bridge.nix new file mode 100644 index 000000000000..f1f8823e8420 --- /dev/null +++ b/nixos/tests/systemd-networkd-bridge.nix @@ -0,0 +1,103 @@ +/* This test ensures that we can configure spanning-tree protocol + across bridges using systemd-networkd. + + Test topology: + + 1 2 3 + node1 --- sw1 --- sw2 --- node2 + \ / + 4 \ / 5 + sw3 + | + 6 | + | + node3 + + where switches 1, 2, and 3 bridge their links and use STP, + and each link is labeled with the VLAN we are assigning it in + virtualisation.vlans. +*/ +with builtins; +let + commonConf = { + systemd.services.systemd-networkd.environment.SYSTEMD_LOG_LEVEL = "debug"; + networking.useNetworkd = true; + networking.useDHCP = false; + networking.firewall.enable = false; + }; + + generateNodeConf = { octet, vlan }: + { lib, pkgs, config, ... }: { + imports = [ common/user-account.nix commonConf ]; + virtualisation.vlans = [ vlan ]; + systemd.network = { + enable = true; + networks = { + "30-eth" = { + matchConfig.Name = "eth1"; + address = [ "10.0.0.${toString octet}/24" ]; + }; + }; + }; + }; + + generateSwitchConf = vlans: + { lib, pkgs, config, ... }: { + imports = [ common/user-account.nix commonConf ]; + virtualisation.vlans = vlans; + systemd.network = { + enable = true; + netdevs = { + "40-br0" = { + netdevConfig = { + Kind = "bridge"; + Name = "br0"; + }; + bridgeConfig.STP = "yes"; + }; + }; + networks = { + "30-eth" = { + matchConfig.Name = "eth*"; + networkConfig.Bridge = "br0"; + }; + "40-br0" = { matchConfig.Name = "br0"; }; + }; + }; + }; +in import ./make-test-python.nix ({ pkgs, ... }: { + name = "networkd"; + meta = with pkgs.lib.maintainers; { maintainers = [ picnoir ]; }; + nodes = { + node1 = generateNodeConf { + octet = 1; + vlan = 1; + }; + node2 = generateNodeConf { + octet = 2; + vlan = 3; + }; + node3 = generateNodeConf { + octet = 3; + vlan = 6; + }; + sw1 = generateSwitchConf [ 1 2 4 ]; + sw2 = generateSwitchConf [ 2 3 5 ]; + sw3 = generateSwitchConf [ 4 5 6 ]; + }; + testScript = '' + network_nodes = [node1, node2, node3] + network_switches = [sw1, sw2, sw3] + start_all() + + for n in network_nodes + network_switches: + n.wait_for_unit("systemd-networkd-wait-online.service") + + node1.succeed("ping 10.0.0.2 -w 10 -c 1") + node1.succeed("ping 10.0.0.3 -w 10 -c 1") + node2.succeed("ping 10.0.0.1 -w 10 -c 1") + node2.succeed("ping 10.0.0.3 -w 10 -c 1") + node3.succeed("ping 10.0.0.1 -w 10 -c 1") + node3.succeed("ping 10.0.0.2 -w 10 -c 1") + ''; +})