From 93502aa3b18db32649814fbb8341ea59ba40df5c Mon Sep 17 00:00:00 2001 From: Graham Dennis Date: Mon, 22 May 2023 13:38:52 +1000 Subject: [PATCH] nixos/qemu-vm: add option for named network interfaces Adds a new option to the virtualisation modules that enables specifying explicitly named network interfaces in QEMU VMs. The existing `virtualisation.vlans` option is still supported for cases where the name of the network interface is irrelevant. --- .../manual/release-notes/rl-2311.section.md | 2 +- nixos/lib/testing/driver.nix | 4 +- nixos/lib/testing/network.nix | 44 +++-- nixos/modules/virtualisation/qemu-vm.nix | 32 +++- nixos/tests/networking.nix | 154 +++++++++--------- 5 files changed, 140 insertions(+), 96 deletions(-) diff --git a/nixos/doc/manual/release-notes/rl-2311.section.md b/nixos/doc/manual/release-notes/rl-2311.section.md index 7e260d2eca04..a68b14d61219 100644 --- a/nixos/doc/manual/release-notes/rl-2311.section.md +++ b/nixos/doc/manual/release-notes/rl-2311.section.md @@ -16,4 +16,4 @@ ## Other Notable Changes {#sec-release-23.11-notable-changes} -- Create the first release note entry in this section! +- A new option was added to the virtualisation module that enables specifying explicitly named network interfaces in QEMU VMs. The existing `virtualisation.vlans` is still supported for cases where the name of the network interface is irrelevant. diff --git a/nixos/lib/testing/driver.nix b/nixos/lib/testing/driver.nix index 25759a91dda3..444236efb1e7 100644 --- a/nixos/lib/testing/driver.nix +++ b/nixos/lib/testing/driver.nix @@ -12,7 +12,9 @@ let }; - vlans = map (m: m.virtualisation.vlans) (lib.attrValues config.nodes); + vlans = map (m: ( + m.virtualisation.vlans ++ + (lib.mapAttrsToList (_: v: v.vlan) m.virtualisation.interfaces))) (lib.attrValues config.nodes); vms = map (m: m.system.build.vm) (lib.attrValues config.nodes); nodeHostNames = diff --git a/nixos/lib/testing/network.nix b/nixos/lib/testing/network.nix index 04ea9a2bc9f7..1ba8e3474ea3 100644 --- a/nixos/lib/testing/network.nix +++ b/nixos/lib/testing/network.nix @@ -4,7 +4,7 @@ let inherit (lib) attrNames concatMap concatMapStrings flip forEach head listToAttrs mkDefault mkOption nameValuePair optionalString - range types zipListsWith zipLists + range toLower types zipListsWith zipLists mdDoc ; @@ -18,24 +18,41 @@ let networkModule = { config, nodes, pkgs, ... }: let - interfacesNumbered = zipLists config.virtualisation.vlans (range 1 255); - interfaces = forEach interfacesNumbered ({ fst, snd }: - nameValuePair "eth${toString snd}" { - ipv4.addresses = - [{ - address = "192.168.${toString fst}.${toString config.virtualisation.test.nodeNumber}"; + qemu-common = import ../qemu-common.nix { inherit lib pkgs; }; + + # Convert legacy VLANs to named interfaces and merge with explicit interfaces. + vlansNumbered = forEach (zipLists config.virtualisation.vlans (range 1 255)) (v: { + name = "eth${toString v.snd}"; + vlan = v.fst; + assignIP = true; + }); + explicitInterfaces = lib.mapAttrsToList (n: v: v // { name = n; }) config.virtualisation.interfaces; + interfaces = vlansNumbered ++ explicitInterfaces; + interfacesNumbered = zipLists interfaces (range 1 255); + + # Automatically assign IP addresses to requested interfaces. + assignIPs = lib.filter (i: i.assignIP) interfaces; + ipInterfaces = forEach assignIPs (i: + nameValuePair i.name { ipv4.addresses = + [ { address = "192.168.${toString i.vlan}.${toString config.virtualisation.test.nodeNumber}"; prefixLength = 24; }]; }); + qemuOptions = lib.flatten (forEach interfacesNumbered ({ fst, snd }: + qemu-common.qemuNICFlags snd fst.vlan config.virtualisation.test.nodeNumber)); + udevRules = forEach interfacesNumbered ({ fst, snd }: + # MAC Addresses for QEMU network devices are lowercase, and udev string comparison is case-sensitive. + "SUBSYSTEM==\"net\",ACTION==\"add\",ATTR{address}==\"${toLower(qemu-common.qemuNicMac fst.vlan config.virtualisation.test.nodeNumber)}\",NAME=\"${fst.name}\""); + networkConfig = { networking.hostName = mkDefault config.virtualisation.test.nodeName; - networking.interfaces = listToAttrs interfaces; + networking.interfaces = listToAttrs ipInterfaces; networking.primaryIPAddress = - optionalString (interfaces != [ ]) (head (head interfaces).value.ipv4.addresses).address; + optionalString (ipInterfaces != [ ]) (head (head ipInterfaces).value.ipv4.addresses).address; # Put the IP addresses of all VMs in this machine's # /etc/hosts file. If a machine has multiple @@ -51,16 +68,13 @@ let "${config.networking.hostName}.${config.networking.domain} " + "${config.networking.hostName}\n")); - virtualisation.qemu.options = - let qemu-common = import ../qemu-common.nix { inherit lib pkgs; }; - in - flip concatMap interfacesNumbered - ({ fst, snd }: qemu-common.qemuNICFlags snd fst config.virtualisation.test.nodeNumber); + virtualisation.qemu.options = qemuOptions; + boot.initrd.services.udev.rules = concatMapStrings (x: x + "\n") udevRules; }; in { - key = "ip-address"; + key = "network-interfaces"; config = networkConfig // { # Expose the networkConfig items for tests like nixops # that need to recreate the network config. diff --git a/nixos/modules/virtualisation/qemu-vm.nix b/nixos/modules/virtualisation/qemu-vm.nix index 5f6bf4b39e97..a8ac478aab4c 100644 --- a/nixos/modules/virtualisation/qemu-vm.nix +++ b/nixos/modules/virtualisation/qemu-vm.nix @@ -564,7 +564,8 @@ in virtualisation.vlans = mkOption { type = types.listOf types.ints.unsigned; - default = [ 1 ]; + default = if config.virtualisation.interfaces == {} then [ 1 ] else [ ]; + defaultText = lib.literalExpression ''if config.virtualisation.interfaces == {} then [ 1 ] else [ ]''; example = [ 1 2 ]; description = lib.mdDoc '' @@ -579,6 +580,35 @@ in ''; }; + virtualisation.interfaces = mkOption { + default = {}; + example = { + enp1s0.vlan = 1; + }; + description = lib.mdDoc '' + Network interfaces to add to the VM. + ''; + type = with types; attrsOf (submodule { + options = { + vlan = mkOption { + type = types.ints.unsigned; + description = lib.mdDoc '' + VLAN to which the network interface is connected. + ''; + }; + + assignIP = mkOption { + type = types.bool; + default = false; + description = lib.mdDoc '' + Automatically assign an IP address to the network interface using the same scheme as + virtualisation.vlans. + ''; + }; + }; + }); + }; + virtualisation.writableStore = mkOption { type = types.bool; diff --git a/nixos/tests/networking.nix b/nixos/tests/networking.nix index 441d258afc08..99f0b6db32af 100644 --- a/nixos/tests/networking.nix +++ b/nixos/tests/networking.nix @@ -93,18 +93,19 @@ let name = "Static"; nodes.router = router; nodes.client = { pkgs, ... }: with pkgs.lib; { - virtualisation.vlans = [ 1 2 ]; + virtualisation.interfaces.enp1s0.vlan = 1; + virtualisation.interfaces.enp2s0.vlan = 2; networking = { useNetworkd = networkd; useDHCP = false; defaultGateway = "192.168.1.1"; defaultGateway6 = "fd00:1234:5678:1::1"; - interfaces.eth1.ipv4.addresses = mkOverride 0 [ + interfaces.enp1s0.ipv4.addresses = [ { address = "192.168.1.2"; prefixLength = 24; } { address = "192.168.1.3"; prefixLength = 32; } { address = "192.168.1.10"; prefixLength = 32; } ]; - interfaces.eth2.ipv4.addresses = mkOverride 0 [ + interfaces.enp2s0.ipv4.addresses = [ { address = "192.168.2.2"; prefixLength = 24; } ]; }; @@ -170,12 +171,12 @@ let # Disable test driver default config networking.interfaces = lib.mkForce {}; networking.useNetworkd = networkd; - virtualisation.vlans = [ 1 ]; + virtualisation.interfaces.enp1s0.vlan = 1; }; testScript = '' start_all() client.wait_for_unit("multi-user.target") - client.wait_until_succeeds("ip addr show dev eth1 | grep '192.168.1'") + client.wait_until_succeeds("ip addr show dev enp1s0 | grep '192.168.1'") client.shell_interact() client.succeed("ping -c 1 192.168.1.1") router.succeed("ping -c 1 192.168.1.1") @@ -187,20 +188,13 @@ let name = "SimpleDHCP"; nodes.router = router; nodes.client = { pkgs, ... }: with pkgs.lib; { - virtualisation.vlans = [ 1 2 ]; + virtualisation.interfaces.enp1s0.vlan = 1; + virtualisation.interfaces.enp2s0.vlan = 2; networking = { useNetworkd = networkd; useDHCP = false; - interfaces.eth1 = { - ipv4.addresses = mkOverride 0 [ ]; - ipv6.addresses = mkOverride 0 [ ]; - useDHCP = true; - }; - interfaces.eth2 = { - ipv4.addresses = mkOverride 0 [ ]; - ipv6.addresses = mkOverride 0 [ ]; - useDHCP = true; - }; + interfaces.enp1s0.useDHCP = true; + interfaces.enp2s0.useDHCP = true; }; }; testScript = { ... }: @@ -211,10 +205,10 @@ let router.wait_for_unit("network-online.target") with subtest("Wait until we have an ip address on each interface"): - client.wait_until_succeeds("ip addr show dev eth1 | grep -q '192.168.1'") - client.wait_until_succeeds("ip addr show dev eth1 | grep -q 'fd00:1234:5678:1:'") - client.wait_until_succeeds("ip addr show dev eth2 | grep -q '192.168.2'") - client.wait_until_succeeds("ip addr show dev eth2 | grep -q 'fd00:1234:5678:2:'") + client.wait_until_succeeds("ip addr show dev enp1s0 | grep -q '192.168.1'") + client.wait_until_succeeds("ip addr show dev enp1s0 | grep -q 'fd00:1234:5678:1:'") + client.wait_until_succeeds("ip addr show dev enp2s0 | grep -q '192.168.2'") + client.wait_until_succeeds("ip addr show dev enp2s0 | grep -q 'fd00:1234:5678:2:'") with subtest("Test vlan 1"): client.wait_until_succeeds("ping -c 1 192.168.1.1") @@ -243,16 +237,15 @@ let name = "OneInterfaceDHCP"; nodes.router = router; nodes.client = { pkgs, ... }: with pkgs.lib; { - virtualisation.vlans = [ 1 2 ]; + virtualisation.interfaces.enp1s0.vlan = 1; + virtualisation.interfaces.enp2s0.vlan = 2; networking = { useNetworkd = networkd; useDHCP = false; - interfaces.eth1 = { - ipv4.addresses = mkOverride 0 [ ]; + interfaces.enp1s0 = { mtu = 1343; useDHCP = true; }; - interfaces.eth2.ipv4.addresses = mkOverride 0 [ ]; }; }; testScript = { ... }: @@ -264,10 +257,10 @@ let router.wait_for_unit("network.target") with subtest("Wait until we have an ip address on each interface"): - client.wait_until_succeeds("ip addr show dev eth1 | grep -q '192.168.1'") + client.wait_until_succeeds("ip addr show dev enp1s0 | grep -q '192.168.1'") with subtest("ensure MTU is set"): - assert "mtu 1343" in client.succeed("ip link show dev eth1") + assert "mtu 1343" in client.succeed("ip link show dev enp1s0") with subtest("Test vlan 1"): client.wait_until_succeeds("ping -c 1 192.168.1.1") @@ -286,16 +279,15 @@ let }; bond = let node = address: { pkgs, ... }: with pkgs.lib; { - virtualisation.vlans = [ 1 2 ]; + virtualisation.interfaces.enp1s0.vlan = 1; + virtualisation.interfaces.enp2s0.vlan = 2; networking = { useNetworkd = networkd; useDHCP = false; bonds.bond0 = { - interfaces = [ "eth1" "eth2" ]; + interfaces = [ "enp1s0" "enp2s0" ]; driverOptions.mode = "802.3ad"; }; - interfaces.eth1.ipv4.addresses = mkOverride 0 [ ]; - interfaces.eth2.ipv4.addresses = mkOverride 0 [ ]; interfaces.bond0.ipv4.addresses = mkOverride 0 [ { inherit address; prefixLength = 30; } ]; }; @@ -326,12 +318,11 @@ let }; bridge = let node = { address, vlan }: { pkgs, ... }: with pkgs.lib; { - virtualisation.vlans = [ vlan ]; + virtualisation.interfaces.enp1s0.vlan = vlan; networking = { useNetworkd = networkd; useDHCP = false; - interfaces.eth1.ipv4.addresses = mkOverride 0 - [ { inherit address; prefixLength = 24; } ]; + interfaces.enp1s0.ipv4.addresses = [ { inherit address; prefixLength = 24; } ]; }; }; in { @@ -339,11 +330,12 @@ let nodes.client1 = node { address = "192.168.1.2"; vlan = 1; }; nodes.client2 = node { address = "192.168.1.3"; vlan = 2; }; nodes.router = { pkgs, ... }: with pkgs.lib; { - virtualisation.vlans = [ 1 2 ]; + virtualisation.interfaces.enp1s0.vlan = 1; + virtualisation.interfaces.enp2s0.vlan = 2; networking = { useNetworkd = networkd; useDHCP = false; - bridges.bridge.interfaces = [ "eth1" "eth2" ]; + bridges.bridge.interfaces = [ "enp1s0" "enp2s0" ]; interfaces.eth1.ipv4.addresses = mkOverride 0 [ ]; interfaces.eth2.ipv4.addresses = mkOverride 0 [ ]; interfaces.bridge.ipv4.addresses = mkOverride 0 @@ -377,7 +369,7 @@ let nodes.router = router; nodes.client = { pkgs, ... }: with pkgs.lib; { environment.systemPackages = [ pkgs.iptables ]; # to debug firewall rules - virtualisation.vlans = [ 1 ]; + virtualisation.interfaces.enp1s0.vlan = 1; networking = { useNetworkd = networkd; useDHCP = false; @@ -385,14 +377,9 @@ let # reverse path filtering rules for the macvlan interface seem # to be incorrect, causing the test to fail. Disable temporarily. firewall.checkReversePath = false; - macvlans.macvlan.interface = "eth1"; - interfaces.eth1 = { - ipv4.addresses = mkOverride 0 [ ]; - useDHCP = true; - }; - interfaces.macvlan = { - useDHCP = true; - }; + macvlans.macvlan.interface = "enp1s0"; + interfaces.enp1s0.useDHCP = true; + interfaces.macvlan.useDHCP = true; }; }; testScript = { ... }: @@ -404,7 +391,7 @@ let router.wait_for_unit("network.target") with subtest("Wait until we have an ip address on each interface"): - client.wait_until_succeeds("ip addr show dev eth1 | grep -q '192.168.1'") + client.wait_until_succeeds("ip addr show dev enp1s0 | grep -q '192.168.1'") client.wait_until_succeeds("ip addr show dev macvlan | grep -q '192.168.1'") with subtest("Print lots of diagnostic information"): @@ -431,23 +418,22 @@ let fou = { name = "foo-over-udp"; nodes.machine = { ... }: { - virtualisation.vlans = [ 1 ]; + virtualisation.interfaces.enp1s0.vlan = 1; networking = { useNetworkd = networkd; useDHCP = false; - interfaces.eth1.ipv4.addresses = mkOverride 0 - [ { address = "192.168.1.1"; prefixLength = 24; } ]; + interfaces.enp1s0.ipv4.addresses = [ { address = "192.168.1.1"; prefixLength = 24; } ]; fooOverUDP = { fou1 = { port = 9001; }; fou2 = { port = 9002; protocol = 41; }; fou3 = mkIf (!networkd) { port = 9003; local.address = "192.168.1.1"; }; fou4 = mkIf (!networkd) - { port = 9004; local = { address = "192.168.1.1"; dev = "eth1"; }; }; + { port = 9004; local = { address = "192.168.1.1"; dev = "enp1s0"; }; }; }; }; systemd.services = { - fou3-fou-encap.after = optional (!networkd) "network-addresses-eth1.service"; + fou3-fou-encap.after = optional (!networkd) "network-addresses-enp1s0.service"; }; }; testScript = { ... }: @@ -470,22 +456,22 @@ let "gue": None, "family": "inet", "local": "192.168.1.1", - "dev": "eth1", + "dev": "enp1s0", } in fous, "fou4 exists" ''; }; sit = let node = { address4, remote, address6 }: { pkgs, ... }: with pkgs.lib; { - virtualisation.vlans = [ 1 ]; + virtualisation.interfaces.enp1s0.vlan = 1; networking = { useNetworkd = networkd; useDHCP = false; sits.sit = { inherit remote; local = address4; - dev = "eth1"; + dev = "enp1s0"; }; - interfaces.eth1.ipv4.addresses = mkOverride 0 + interfaces.enp1s0.ipv4.addresses = mkOverride 0 [ { address = address4; prefixLength = 24; } ]; interfaces.sit.ipv6.addresses = mkOverride 0 [ { address = address6; prefixLength = 64; } ]; @@ -685,10 +671,10 @@ let vlan-ping = let baseIP = number: "10.10.10.${number}"; vlanIP = number: "10.1.1.${number}"; - baseInterface = "eth1"; + baseInterface = "enp1s0"; vlanInterface = "vlan42"; node = number: {pkgs, ... }: with pkgs.lib; { - virtualisation.vlans = [ 1 ]; + virtualisation.interfaces.enp1s0.vlan = 1; networking = { #useNetworkd = networkd; useDHCP = false; @@ -785,12 +771,12 @@ let privacy = { name = "Privacy"; nodes.router = { ... }: { - virtualisation.vlans = [ 1 ]; + virtualisation.interfaces.enp1s0.vlan = 1; boot.kernel.sysctl."net.ipv6.conf.all.forwarding" = true; networking = { useNetworkd = networkd; useDHCP = false; - interfaces.eth1.ipv6.addresses = singleton { + interfaces.enp1s0.ipv6.addresses = singleton { address = "fd00:1234:5678:1::1"; prefixLength = 64; }; @@ -798,7 +784,7 @@ let services.radvd = { enable = true; config = '' - interface eth1 { + interface enp1s0 { AdvSendAdvert on; AdvManagedFlag on; AdvOtherConfigFlag on; @@ -812,11 +798,11 @@ let }; }; nodes.client_with_privacy = { pkgs, ... }: with pkgs.lib; { - virtualisation.vlans = [ 1 ]; + virtualisation.interfaces.enp1s0.vlan = 1; networking = { useNetworkd = networkd; useDHCP = false; - interfaces.eth1 = { + interfaces.enp1s0 = { tempAddress = "default"; ipv4.addresses = mkOverride 0 [ ]; ipv6.addresses = mkOverride 0 [ ]; @@ -825,11 +811,11 @@ let }; }; nodes.client = { pkgs, ... }: with pkgs.lib; { - virtualisation.vlans = [ 1 ]; + virtualisation.interfaces.enp1s0.vlan = 1; networking = { useNetworkd = networkd; useDHCP = false; - interfaces.eth1 = { + interfaces.enp1s0 = { tempAddress = "enabled"; ipv4.addresses = mkOverride 0 [ ]; ipv6.addresses = mkOverride 0 [ ]; @@ -847,9 +833,9 @@ let with subtest("Wait until we have an ip address"): client_with_privacy.wait_until_succeeds( - "ip addr show dev eth1 | grep -q 'fd00:1234:5678:1:'" + "ip addr show dev enp1s0 | grep -q 'fd00:1234:5678:1:'" ) - client.wait_until_succeeds("ip addr show dev eth1 | grep -q 'fd00:1234:5678:1:'") + client.wait_until_succeeds("ip addr show dev enp1s0 | grep -q 'fd00:1234:5678:1:'") with subtest("Test vlan 1"): client_with_privacy.wait_until_succeeds("ping -c 1 fd00:1234:5678:1::1") @@ -947,7 +933,7 @@ let ), "The IPv6 routing table has not been properly cleaned:\n{}".format(ipv6Residue) ''; }; - rename = { + rename = if networkd then { name = "RenameInterface"; nodes.machine = { pkgs, ... }: { virtualisation.vlans = [ 1 ]; @@ -955,23 +941,20 @@ let useNetworkd = networkd; useDHCP = false; }; - } // - (if networkd - then { systemd.network.links."10-custom_name" = { - matchConfig.MACAddress = "52:54:00:12:01:01"; - linkConfig.Name = "custom_name"; - }; - } - else { boot.initrd.services.udev.rules = '' - SUBSYSTEM=="net", ACTION=="add", DRIVERS=="?*", ATTR{address}=="52:54:00:12:01:01", KERNEL=="eth*", NAME="custom_name" - ''; - }); + systemd.network.links."10-custom_name" = { + matchConfig.MACAddress = "52:54:00:12:01:01"; + linkConfig.Name = "custom_name"; + }; + }; testScript = '' machine.succeed("udevadm settle") print(machine.succeed("ip link show dev custom_name")) ''; - }; + } else { + name = "RenameInterface"; nodes = { }; + testScript = ""; + }; # even with disabled networkd, systemd.network.links should work # (as it's handled by udev, not networkd) link = { @@ -1015,6 +998,21 @@ let machine.fail("ip address show wlan0 | grep -q ${testMac}") ''; }; + caseSensitiveRenaming = { + name = "CaseSensitiveRenaming"; + nodes.machine = { pkgs, ... }: { + virtualisation.interfaces.enCustom.vlan = 11; + networking = { + useNetworkd = networkd; + useDHCP = false; + }; + }; + testScript = '' + machine.succeed("udevadm settle") + print(machine.succeed("ip link show dev enCustom")) + machine.wait_until_succeeds("ip link show dev enCustom | grep -q '52:54:00:12:0b:01") + ''; + }; }; in mapAttrs (const (attrs: makeTest (attrs // {