{
  system ? builtins.currentSystem,
  config ? { },
  pkgs ? import ../.. { inherit system config; },
  # bool: whether to use networkd in the tests
  networkd,
}:

with import ../../lib/testing-python.nix { inherit system pkgs; };

let
  lib = pkgs.lib;
  router = import ./router.nix { inherit networkd; };
  clientConfig =
    extraConfig:
    lib.recursiveUpdate {
      networking.useDHCP = false;
      networking.useNetworkd = networkd;
    } extraConfig;
  testCases = {
    loopback = {
      name = "Loopback";
      nodes.client = clientConfig { };
      testScript = ''
        start_all()
        client.wait_for_unit("network.target")
        loopback_addresses = client.succeed("ip addr show lo")
        assert "inet 127.0.0.1/8" in loopback_addresses
        assert "inet6 ::1/128" in loopback_addresses
      '';
    };
    static = {
      name = "Static";
      nodes.router = router;
      nodes.client = clientConfig {
        virtualisation.interfaces.enp1s0.vlan = 1;
        virtualisation.interfaces.enp2s0.vlan = 2;
        networking = {
          defaultGateway = {
            address = "192.168.1.1";
            interface = "enp1s0";
            source = "192.168.1.3";
          };
          defaultGateway6 = {
            address = "fd00:1234:5678:1::1";
            interface = "enp1s0";
            source = "fd00:1234:5678:1::3"; # implicit dependency on enp2s0
          };
          interfaces.enp1s0.ipv6.addresses = [
            {
              address = "fd00:1234:5678:1::2";
              prefixLength = 64;
            }
          ];
          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.enp2s0.ipv4.addresses = [
            {
              address = "192.168.2.2";
              prefixLength = 24;
            }
          ];
          interfaces.enp2s0.ipv6.addresses = [
            {
              address = "fd00:1234:5678:1::3";
              prefixLength = 128;
            }
          ];
        };
      };
      testScript = ''
        start_all()

        client.wait_for_unit("network.target")
        router.systemctl("start network-online.target")
        router.wait_for_unit("network-online.target")

        with subtest("Make sure DHCP server is not started"):
            client.fail("systemctl status kea-dhcp4-server.service")
            client.fail("systemctl status kea-dhcp6-server.service")

        with subtest("Test vlan 1"):
            client.wait_until_succeeds("ping -c 1 192.168.1.1")
            router.wait_until_succeeds("ping -c 1 192.168.1.2")
            router.wait_until_succeeds("ping -c 1 192.168.1.3")
            router.wait_until_succeeds("ping -c 1 192.168.1.10")

        with subtest("Test vlan 2"):
            client.wait_until_succeeds("ping -c 1 192.168.2.1")
            router.wait_until_succeeds("ping -c 1 192.168.2.2")

        with subtest("Test default gateway"):
            client.wait_until_succeeds("ping -c 1 192.168.3.1")
            client.wait_until_succeeds("ping -c 1 fd00:1234:5678:1::1")

        with subtest("Test default addresses"):
            client.succeed("ip -4 route show default | grep -q 'src 192.168.1.3'")
            client.succeed("ip -6 route show default | grep -q 'src fd00:1234:5678:1::3'")
      '';
    };
    dynamicInterface = {
      name = "dynamicInterface";
      nodes.machine = clientConfig {
        networking.interfaces.usb0 = {
          ipv6.addresses = lib.singleton {
            address = "fd::1";
            prefixLength = 127;
          };
        };
        networking.defaultGateway6 = {
          address = "fd::";
          interface = "usb0";
          source = "fd::1";
        };
      };
      testScript = ''
        with subtest("Network comes up without usb0"):
          machine.wait_for_unit("network.target")

        with subtest("multi-user.target does not hang"):
          machine.require_unit_state("multi-user.target", "active")

        with subtest("usb0 is configured when plugged in"):
          machine.succeed("ip link add usb0 type sit local 1.2.3.4")
          machine.wait_until_succeeds("ip addr show dev usb0 | grep -q fd::1")

        with subtest("Network is now online"):
          machine.systemctl("start network-online.target")
          machine.require_unit_state("network-online.target", "active")

        with subtest("Default gateway is now set"):
          machine.succeed("ip -6 route show default | grep -q 'via fd::'")
          machine.succeed("ip -6 route show default | grep -q 'src fd::1'")
      '';
    };
    routeType = {
      name = "RouteType";
      nodes.client = clientConfig {
        networking = {
          interfaces.eth1.ipv4.routes = [
            {
              address = "192.168.1.127";
              prefixLength = 32;
              type = "local";
            }
          ];
        };
      };
      testScript = ''
        start_all()
        client.wait_for_unit("network.target")
        client.succeed("ip -4 route list table local | grep 'local 192.168.1.127'")
      '';
    };
    dhcpDefault = {
      name = "useDHCP-by-default";
      nodes.router = router;
      nodes.client = {
        # Disable test driver default config
        networking.interfaces = lib.mkForce {
          # Make sure DHCP defaults correctly even when some unrelated config
          # is set on the interface (nothing, in this case).
          enp1s0 = { };
        };
        networking.useNetworkd = networkd;
        virtualisation.interfaces.enp1s0.vlan = 1;
      };
      testScript = ''
        start_all()
        client.wait_for_unit("multi-user.target")
        client.wait_until_succeeds("ip addr show dev enp1s0 | grep '192.168.1'")
        router.succeed("ping -c 1 192.168.1.1")
        client.succeed("ping -c 1 192.168.1.2")
      '';
    };
    dhcpSimple = {
      name = "SimpleDHCP";
      nodes.router = router;
      nodes.client = clientConfig {
        virtualisation.interfaces.enp1s0.vlan = 1;
        virtualisation.interfaces.enp2s0.vlan = 2;
        networking = {
          interfaces.enp1s0.useDHCP = true;
          interfaces.enp2s0.useDHCP = true;
        };
      };
      testScript = ''
        router.start()
        router.systemctl("start network-online.target")
        router.wait_for_unit("network-online.target")

        client.start()
        client.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 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("Wait until we have received the nameservers"):
          if "${builtins.toJSON networkd}" == "true":
            client.wait_until_succeeds("resolvectl status enp2s0 | grep -q 2001:db8::1")
            client.wait_until_succeeds("resolvectl status enp2s0 | grep -q 192.168.2.1")
          else:
            client.wait_until_succeeds("resolvconf -l | grep -q 2001:db8::1")
            client.wait_until_succeeds("resolvconf -l | grep -q 192.168.2.1")

        with subtest("Test vlan 1"):
            client.wait_until_succeeds("ping -c 1 192.168.1.1")
            client.wait_until_succeeds("ping -c 1 fd00:1234:5678:1::1")
            router.wait_until_succeeds("ping -c 1 192.168.1.2")
            router.wait_until_succeeds("ping -c 1 fd00:1234:5678:1::2")

        with subtest("Test vlan 2"):
            client.wait_until_succeeds("ping -c 1 192.168.2.1")
            client.wait_until_succeeds("ping -c 1 fd00:1234:5678:2::1")
            router.wait_until_succeeds("ping -c 1 192.168.2.2")
            router.wait_until_succeeds("ping -c 1 fd00:1234:5678:2::2")
      '';
    };
    dhcpHostname = {
      name = "hostnameDHCP";
      nodes.router = router;
      nodes.client = clientConfig {
        # use the name given by the DHCP server
        system.name = "client";
        networking.hostName = lib.mkForce "";
        security.polkit.enable = true;
        virtualisation.interfaces.enp1s0.vlan = 1;
        networking.interfaces.enp1s0.useDHCP = true;
      };
      testScript = ''
        router.start()
        router.systemctl("start network-online.target")
        router.wait_for_unit("network-online.target")

        client.start()
        client.wait_for_unit("network.target")

        with subtest("Wait until we have received the hostname"):
            client.wait_until_succeeds("hostname | grep -q 'client1'")
      '';
    };
    dhcpOneIf = {
      name = "OneInterfaceDHCP";
      nodes.router = router;
      nodes.client = clientConfig {
        virtualisation.interfaces.enp1s0.vlan = 1;
        virtualisation.interfaces.enp2s0.vlan = 2;
        networking = {
          interfaces.enp1s0 = {
            mtu = 1343;
            useDHCP = true;
          };
        };
      };
      testScript = ''
        start_all()

        with subtest("Wait for networking to come up"):
            client.wait_for_unit("network.target")
            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 enp1s0 | grep -q '192.168.1'")

        with subtest("ensure MTU is set"):
            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")
            router.wait_until_succeeds("ping -c 1 192.168.1.2")

        with subtest("Test vlan 2"):
            client.wait_until_succeeds("ping -c 1 192.168.2.1")
            client.fail("ping -c 1 192.168.2.2")

            router.wait_until_succeeds("ping -c 1 192.168.2.1")
            router.fail("ping -c 1 192.168.2.2")
      '';
    };
    bond =
      let
        node =
          address:
          clientConfig {
            virtualisation.interfaces.enp1s0.vlan = 1;
            virtualisation.interfaces.enp2s0.vlan = 2;
            networking = {
              bonds.bond0 = {
                interfaces = [
                  "enp1s0"
                  "enp2s0"
                ];
                driverOptions.mode = "802.3ad";
              };
              interfaces.bond0.ipv4.addresses = lib.mkOverride 0 [
                {
                  inherit address;
                  prefixLength = 30;
                }
              ];
            };

            # virtio-net reports its speed and duplex as "unknown" by default,
            # which confuses the 802.3ad logic. However, you can just tell it
            # to pretend to have any link speed with ethtool, so do that.
            systemd.services.fake-link-settings = {
              path = [ pkgs.ethtool ];
              script = ''
                ethtool -s enp1s0 speed 1000 duplex full
                ethtool -s enp2s0 speed 1000 duplex full
              '';
              wantedBy = [ "network-pre.target" ];
            };
          };
      in
      {
        name = "Bond";
        nodes.client1 = node "192.168.1.1";
        nodes.client2 = node "192.168.1.2";
        testScript = ''
          start_all()

          with subtest("Wait for networking to come up"):
              client1.wait_for_unit("network.target")
              client2.wait_for_unit("network.target")

          with subtest("Test bonding"):
              client1.wait_until_succeeds("ping -c2 -w2 192.168.1.1")
              client1.wait_until_succeeds("ping -c2 -w2 192.168.1.2")

              client2.wait_until_succeeds("ping -c2 -w2 192.168.1.1")
              client2.wait_until_succeeds("ping -c2 -w2 192.168.1.2")

          with subtest("Verify bonding mode"):
              for client in client1, client2:
                  client.succeed('grep -q "Bonding Mode: IEEE 802.3ad Dynamic link aggregation" /proc/net/bonding/bond0')
        '';
      };
    bridge =
      let
        node =
          { address, vlan }:
          { pkgs, ... }:
          {
            virtualisation.interfaces.enp1s0.vlan = vlan;
            networking = {
              useNetworkd = networkd;
              useDHCP = false;
              interfaces.enp1s0.ipv4.addresses = [
                {
                  inherit address;
                  prefixLength = 24;
                }
              ];
            };
          };
      in
      {
        name = "Bridge";
        nodes.client1 = node {
          address = "192.168.1.2";
          vlan = 1;
        };
        nodes.client2 = node {
          address = "192.168.1.3";
          vlan = 2;
        };
        nodes.router = {
          virtualisation.interfaces.enp1s0.vlan = 1;
          virtualisation.interfaces.enp2s0.vlan = 2;
          networking = {
            useNetworkd = networkd;
            useDHCP = false;
            bridges.bridge.interfaces = [
              "enp1s0"
              "enp2s0"
            ];
            interfaces.eth1.ipv4.addresses = lib.mkOverride 0 [ ];
            interfaces.eth2.ipv4.addresses = lib.mkOverride 0 [ ];
            interfaces.bridge.ipv4.addresses = lib.mkOverride 0 [
              {
                address = "192.168.1.1";
                prefixLength = 24;
              }
            ];
          };
        };
        testScript = ''
          start_all()

          with subtest("Wait for networking to come up"):
              for machine in client1, client2, router:
                  machine.wait_for_unit("network.target")

          with subtest("Test bridging"):
              client1.wait_until_succeeds("ping -c 1 192.168.1.1")
              client1.wait_until_succeeds("ping -c 1 192.168.1.2")
              client1.wait_until_succeeds("ping -c 1 192.168.1.3")

              client2.wait_until_succeeds("ping -c 1 192.168.1.1")
              client2.wait_until_succeeds("ping -c 1 192.168.1.2")
              client2.wait_until_succeeds("ping -c 1 192.168.1.3")

              router.wait_until_succeeds("ping -c 1 192.168.1.1")
              router.wait_until_succeeds("ping -c 1 192.168.1.2")
              router.wait_until_succeeds("ping -c 1 192.168.1.3")
        '';
      };
    macvlan = {
      name = "MACVLAN";
      nodes.router = router;
      nodes.client =
        { pkgs, ... }:
        {
          environment.systemPackages = [ pkgs.iptables ]; # to debug firewall rules
          virtualisation.interfaces.enp1s0.vlan = 1;
          networking = {
            useNetworkd = networkd;
            useDHCP = false;
            firewall.logReversePathDrops = true; # to debug firewall rules
            # 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 = "enp1s0";
            interfaces.enp1s0.useDHCP = true;
            interfaces.macvlan.useDHCP = true;
          };
        };
      testScript = ''
        start_all()

        with subtest("Wait for networking to come up"):
            client.wait_for_unit("network.target")
            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 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"):
            router.log("**********************************************")
            router.succeed("ip addr >&2")
            router.succeed("ip route >&2")
            router.execute("iptables-save >&2")
            client.log("==============================================")
            client.succeed("ip addr >&2")
            client.succeed("ip route >&2")
            client.execute("iptables-save >&2")
            client.log("##############################################")

        with subtest("Test macvlan creates routable ips"):
            client.wait_until_succeeds("ping -c 1 192.168.1.1")
            client.wait_until_succeeds("ping -c 1 192.168.1.2")
            client.wait_until_succeeds("ping -c 1 192.168.1.3")

            router.wait_until_succeeds("ping -c 1 192.168.1.1")
            router.wait_until_succeeds("ping -c 1 192.168.1.2")
            router.wait_until_succeeds("ping -c 1 192.168.1.3")
      '';
    };
    ipvlan = {
      name = "IPVLAN";
      nodes.client = {
        virtualisation.interfaces.enp1s0.vlan = 1;
        networking = {
          useNetworkd = networkd;
          useDHCP = false;
          ipvlans = {
            ipvlan1.interface = "enp1s0";
            ipvlan2.interface = "enp1s0";
          };
        };
      };
      testScript = ''
        with subtest("Wait for networking to come up"):
            client.wait_for_unit("network.target")

        with subtest("Can move IPVLANs to separate network namespaces"):
            client.succeed("ip netns add ns1 && ip link set dev ipvlan1 netns ns1")
            client.succeed("ip netns add ns2 && ip link set dev ipvlan2 netns ns2")

        with subtest("Can configure the IPVLAN interfaces"):
            client.succeed("ip netns exec ns1 ip addr add 192.168.1.1/24 dev ipvlan1")
            client.succeed("ip netns exec ns2 ip addr add 192.168.1.2/24 dev ipvlan2")
            client.succeed("ip netns exec ns1 ip link set dev ipvlan1 up")
            client.succeed("ip netns exec ns2 ip link set dev ipvlan2 up")

        with subtest("IPVLAN interfaces can ping each other"):
            client.succeed("ip netns exec ns1 ping -c 1 192.168.1.2")
            client.succeed("ip netns exec ns2 ping -c 1 192.168.1.1")
      '';
    };
    fou = {
      name = "foo-over-udp";
      nodes.machine = clientConfig {
        virtualisation.interfaces.enp1s0.vlan = 1;
        networking = {
          interfaces.enp1s0.ipv4.addresses = [
            {
              address = "192.168.1.1";
              prefixLength = 24;
            }
          ];
          fooOverUDP = {
            fou1 = {
              port = 9001;
            };
            fou2 = {
              port = 9002;
              protocol = 41;
            };
            fou3 = lib.mkIf (!networkd) {
              port = 9003;
              local.address = "192.168.1.1";
            };
            fou4 = lib.mkIf (!networkd) {
              port = 9004;
              local = {
                address = "192.168.1.1";
                dev = "enp1s0";
              };
            };
          };
        };
        systemd.services = {
          fou3-fou-encap.after = lib.optional (!networkd) "network-addresses-enp1s0.service";
        };
      };
      testScript = ''
        import json

        machine.wait_for_unit("network.target")
        fous = json.loads(machine.succeed("ip -json fou show"))
        assert {"port": 9001, "gue": None, "family": "inet"} in fous, "fou1 exists"
        assert {"port": 9002, "ipproto": 41, "family": "inet"} in fous, "fou2 exists"
      ''
      + lib.optionalString (!networkd) ''
        assert {
            "port": 9003,
            "gue": None,
            "family": "inet",
            "local": "192.168.1.1",
        } in fous, "fou3 exists"
        assert {
            "port": 9004,
            "gue": None,
            "family": "inet",
            "local": "192.168.1.1",
            "dev": "enp1s0",
        } in fous, "fou4 exists"
      '';
    };
    sit-6in4 =
      let
        node =
          {
            address4,
            remote,
            address6,
          }:
          {
            virtualisation.interfaces.enp1s0.vlan = 1;
            networking = {
              useNetworkd = networkd;
              useDHCP = false;
              sits.sit = {
                inherit remote;
                local = address4;
                dev = "enp1s0";
              };
              nftables.enable = true;
              firewall.extraInputRules = "meta l4proto 41 accept";
              interfaces.enp1s0.ipv4.addresses = lib.mkOverride 0 [
                {
                  address = address4;
                  prefixLength = 24;
                }
              ];
              interfaces.sit.ipv6.addresses = lib.mkOverride 0 [
                {
                  address = address6;
                  prefixLength = 64;
                }
              ];
            };
          };
      in
      {
        name = "Sit-6in4";
        nodes.client1 = node {
          address4 = "192.168.1.1";
          remote = "192.168.1.2";
          address6 = "fc00::1";
        };
        nodes.client2 = node {
          address4 = "192.168.1.2";
          remote = "192.168.1.1";
          address6 = "fc00::2";
        };
        testScript = ''
          start_all()

          with subtest("Wait for networking to be configured"):
              client1.wait_for_unit("network.target")
              client2.wait_for_unit("network.target")

              # Print diagnostic information
              client1.succeed("ip addr >&2")
              client2.succeed("ip addr >&2")

          with subtest("Test ipv6"):
              client1.wait_until_succeeds("ping -c 1 fc00::1")
              client1.wait_until_succeeds("ping -c 1 fc00::2")

              client2.wait_until_succeeds("ping -c 1 fc00::1")
              client2.wait_until_succeeds("ping -c 1 fc00::2")
        '';
      };
    sit-fou =
      let
        node =
          {
            address4,
            remote,
            address6,
          }:
          { pkgs, ... }:
          {
            virtualisation.interfaces.enp1s0.vlan = 1;
            networking = {
              useNetworkd = networkd;
              useDHCP = false;
              sits.sit = {
                inherit remote;
                local = address4;
                dev = "enp1s0";
              };
              interfaces.enp1s0.ipv4.addresses = lib.mkOverride 0 [
                {
                  address = address4;
                  prefixLength = 24;
                }
              ];
              interfaces.sit.ipv6.addresses = lib.mkOverride 0 [
                {
                  address = address6;
                  prefixLength = 64;
                }
              ];
            };
          };
      in
      {
        name = "Sit-fou";
        # note on firewalling: the two nodes are explicitly asymmetric.
        # client1 sends SIT packets in UDP, but accepts only proto-41 incoming.
        # client2 does the reverse, sending in proto-41 and accepting only UDP incoming.
        # that way we'll notice when either SIT itself or FOU breaks.
        nodes.client1 =
          args@{ pkgs, ... }:
          lib.mkMerge [
            (node {
              address4 = "192.168.1.1";
              remote = "192.168.1.2";
              address6 = "fc00::1";
            } args)
            {
              networking = {
                nftables.enable = true;
                firewall.extraInputRules = "meta l4proto 41 accept";
                sits.sit.encapsulation = {
                  type = "fou";
                  port = 9001;
                };
              };
            }
          ];
        nodes.client2 =
          args@{ pkgs, ... }:
          lib.mkMerge [
            (node {
              address4 = "192.168.1.2";
              remote = "192.168.1.1";
              address6 = "fc00::2";
            } args)
            {
              networking = {
                firewall.allowedUDPPorts = [ 9001 ];
                fooOverUDP.fou1 = {
                  port = 9001;
                  protocol = 41;
                };
              };
            }
          ];
        testScript = ''
          start_all()

          with subtest("Wait for networking to be configured"):
              client1.wait_for_unit("network.target")
              client2.wait_for_unit("network.target")

              # Print diagnostic information
              client1.succeed("ip addr >&2")
              client2.succeed("ip addr >&2")

          with subtest("Test ipv6"):
              client1.wait_until_succeeds("ping -c 1 fc00::1")
              client1.wait_until_succeeds("ping -c 1 fc00::2")

              client2.wait_until_succeeds("ping -c 1 fc00::1")
              client2.wait_until_succeeds("ping -c 1 fc00::2")
        '';
      };
    ipip-4in6 =
      let
        node =
          {
            address4,
            remote,
            address6,
          }:
          {
            virtualisation.interfaces.enp1s0.vlan = 1;
            networking = {
              useNetworkd = networkd;
              useDHCP = false;
              ipips."4in6" = {
                inherit remote;
                local = address6;
                dev = "enp1s0";
                encapsulation.type = "4in6";
              };
              firewall.enable = false;
              nftables.enable = true;
              firewall.extraInputRules = "meta l4proto ipip accept";
              interfaces.enp1s0.ipv6.addresses = lib.mkOverride 0 [
                {
                  address = address6;
                  prefixLength = 64;
                }
              ];
              interfaces."4in6".ipv4.addresses = lib.mkOverride 0 [
                {
                  address = address4;
                  prefixLength = 24;
                }
              ];
            };
          };
      in
      {
        name = "ipip-4in6";
        nodes.client1 = node {
          address6 = "fc00::1";
          address4 = "192.168.1.1";
          remote = "fc00::2";
        };
        nodes.client2 = node {
          address6 = "fc00::2";
          address4 = "192.168.1.2";
          remote = "fc00::1";
        };
        testScript = ''
          start_all()

          with subtest("Wait for networking to be configured"):
              client1.wait_for_unit("network.target")
              client2.wait_for_unit("network.target")

              # Print diagnostic information
              client1.succeed("ip addr >&2")
              client2.succeed("ip addr >&2")

          with subtest("Test ipv6"):
              client1.wait_until_succeeds("ping -c 1 192.168.1.1")
              client1.wait_until_succeeds("ping -c 1 192.168.1.2")

              client2.wait_until_succeeds("ping -c 1 192.168.1.1")
              client2.wait_until_succeeds("ping -c 1 192.168.1.2")
        '';
      };
    ipip =
      let
        node =
          {
            local,
            remote,
            address,
          }:
          {
            virtualisation.interfaces.enp1s0.vlan = 1;
            networking = {
              useNetworkd = networkd;
              useDHCP = false;
              ipips.ipip = {
                inherit local remote;
                dev = "enp1s0";
                encapsulation.type = "ipip";
              };
              nftables.enable = true;
              firewall.extraInputRules = "meta l4proto 4 accept";
              interfaces.enp1s0.ipv4.addresses = lib.mkOverride 0 [
                {
                  address = local;
                  prefixLength = 24;
                }
              ];
              interfaces.ipip.ipv4.addresses = lib.mkOverride 0 [
                {
                  inherit address;
                  prefixLength = 24;
                }
              ];
            };
          };
      in
      {
        name = "ipip";
        nodes.client1 = node {
          local = "192.168.1.1";
          remote = "192.168.1.2";
          address = "192.168.10.1";
        };
        nodes.client2 = node {
          local = "192.168.1.2";
          remote = "192.168.1.1";
          address = "192.168.10.2";
        };
        testScript = ''
          start_all()

          with subtest("Wait for networking to be configured"):
              client1.wait_for_unit("network.target")
              client2.wait_for_unit("network.target")

              # Print diagnostic information
              client1.succeed("ip addr >&2")
              client2.succeed("ip addr >&2")

          with subtest("Test IPIP tunnel"):
              client1.wait_until_succeeds("ping -c 1 192.168.10.1")
              client1.wait_until_succeeds("ping -c 1 192.168.10.2")

              client2.wait_until_succeeds("ping -c 1 192.168.10.1")
              client2.wait_until_succeeds("ping -c 1 192.168.10.2")
        '';
      };
    gre =
      let
        node =
          { ... }:
          {
            networking = {
              useNetworkd = networkd;
              useDHCP = false;
              firewall.extraCommands = "ip6tables -A nixos-fw -p gre -j nixos-fw-accept";
            };
          };
      in
      {
        name = "GRE";
        nodes.client1 =
          args@{ pkgs, ... }:
          lib.mkMerge [
            (node args)
            {
              virtualisation.vlans = [
                1
                2
                4
              ];
              networking = {
                greTunnels = {
                  greTunnel = {
                    local = "192.168.2.1";
                    remote = "192.168.2.2";
                    dev = "eth2";
                    ttl = 225;
                    type = "tap";
                  };
                  gre6Tunnel = {
                    local = "fd00:1234:5678:4::1";
                    remote = "fd00:1234:5678:4::2";
                    dev = "eth3";
                    ttl = 255;
                    type = "tun6";
                  };
                };
                bridges.bridge.interfaces = [
                  "greTunnel"
                  "eth1"
                ];
                interfaces.eth1.ipv4.addresses = lib.mkOverride 0 [ ];
                interfaces.eth1.ipv6.addresses = lib.mkOverride 0 [ ];
                interfaces.bridge.ipv4.addresses = lib.mkOverride 0 [
                  {
                    address = "192.168.1.1";
                    prefixLength = 24;
                  }
                ];
                interfaces.eth3.ipv6.addresses = [
                  {
                    address = "fd00:1234:5678:4::1";
                    prefixLength = 64;
                  }
                ];
                interfaces.gre6Tunnel.ipv6.addresses = lib.mkOverride 0 [
                  {
                    address = "fc00::1";
                    prefixLength = 64;
                  }
                ];
              };
            }
          ];
        nodes.client2 =
          args@{ pkgs, ... }:
          lib.mkMerge [
            (node args)
            {
              virtualisation.vlans = [
                2
                3
                4
              ];
              networking = {
                greTunnels = {
                  greTunnel = {
                    local = "192.168.2.2";
                    remote = "192.168.2.1";
                    dev = "eth1";
                    ttl = 225;
                    type = "tap";
                  };
                  gre6Tunnel = {
                    local = "fd00:1234:5678:4::2";
                    remote = "fd00:1234:5678:4::1";
                    dev = "eth3";
                    ttl = 255;
                    type = "tun6";
                  };
                };
                bridges.bridge.interfaces = [
                  "greTunnel"
                  "eth2"
                ];
                interfaces.eth2.ipv4.addresses = lib.mkOverride 0 [ ];
                interfaces.eth2.ipv6.addresses = lib.mkOverride 0 [ ];
                interfaces.bridge.ipv4.addresses = lib.mkOverride 0 [
                  {
                    address = "192.168.1.2";
                    prefixLength = 24;
                  }
                ];
                interfaces.eth3.ipv6.addresses = [
                  {
                    address = "fd00:1234:5678:4::2";
                    prefixLength = 64;
                  }
                ];
                interfaces.gre6Tunnel.ipv6.addresses = lib.mkOverride 0 [
                  {
                    address = "fc00::2";
                    prefixLength = 64;
                  }
                ];
              };
            }
          ];
        testScript = ''
          import json
          start_all()

          with subtest("Wait for networking to be configured"):
              client1.wait_for_unit("network.target")
              client2.wait_for_unit("network.target")

              # Print diagnostic information
              client1.succeed("ip addr >&2")
              client2.succeed("ip addr >&2")

          with subtest("Test GRE tunnel bridge over VLAN"):
              client1.wait_until_succeeds("ping -c 1 192.168.1.2")

              client2.wait_until_succeeds("ping -c 1 192.168.1.1")

              client1.wait_until_succeeds("ping -c 1 fc00::2")

              client2.wait_until_succeeds("ping -c 1 fc00::1")

          with subtest("Test GRE tunnel TTL"):
              links = json.loads(client1.succeed("ip -details -json link show greTunnel"))
              assert links[0]['linkinfo']['info_data']['ttl'] == 225, "ttl not set for greTunnel"

              links = json.loads(client2.succeed("ip -details -json link show gre6Tunnel"))
              assert links[0]['linkinfo']['info_data']['ttl'] == 255, "ttl not set for gre6Tunnel"
        '';
      };
    vlan =
      let
        node = address: {
          networking = {
            useNetworkd = networkd;
            useDHCP = false;
            vlans.vlan = {
              id = 1;
              interface = "eth0";
            };
            interfaces.eth0.ipv4.addresses = lib.mkOverride 0 [ ];
            interfaces.eth1.ipv4.addresses = lib.mkOverride 0 [ ];
            interfaces.vlan.ipv4.addresses = lib.mkOverride 0 [
              {
                inherit address;
                prefixLength = 24;
              }
            ];
          };
        };
      in
      {
        name = "vlan";
        nodes.client1 = node "192.168.1.1";
        nodes.client2 = node "192.168.1.2";
        testScript = ''
          start_all()

          with subtest("Wait for networking to be configured"):
              client1.wait_for_unit("network.target")
              client2.wait_for_unit("network.target")

          with subtest("Test vlan is setup"):
              client1.succeed("ip addr show dev vlan >&2")
              client2.succeed("ip addr show dev vlan >&2")
        '';
      };
    vlan-ping =
      let
        baseIP = number: "10.10.10.${number}";
        vlanIP = number: "10.1.1.${number}";
        baseInterface = "enp1s0";
        vlanInterface = "vlan42";
        node = number: {
          virtualisation.interfaces.enp1s0.vlan = 1;
          networking = {
            #useNetworkd = networkd;
            useDHCP = false;
            vlans.${vlanInterface} = {
              id = 42;
              interface = baseInterface;
            };
            interfaces.${baseInterface}.ipv4.addresses = lib.mkOverride 0 [
              {
                address = baseIP number;
                prefixLength = 24;
              }
            ];
            interfaces.${vlanInterface}.ipv4.addresses = lib.mkOverride 0 [
              {
                address = vlanIP number;
                prefixLength = 24;
              }
            ];
          };
        };

        serverNodeNum = "1";
        clientNodeNum = "2";

      in
      {
        name = "vlan-ping";
        nodes.server = node serverNodeNum;
        nodes.client = node clientNodeNum;
        testScript = ''
          start_all()

          with subtest("Wait for networking to be configured"):
              server.wait_for_unit("network.target")
              client.wait_for_unit("network.target")

          with subtest("Test ping on base interface in setup"):
              client.succeed("ping -I ${baseInterface} -c 1 ${baseIP serverNodeNum}")
              server.succeed("ping -I ${baseInterface} -c 1 ${baseIP clientNodeNum}")

          with subtest("Test ping on vlan subinterface in setup"):
              client.succeed("ping -I ${vlanInterface} -c 1 ${vlanIP serverNodeNum}")
              server.succeed("ping -I ${vlanInterface} -c 1 ${vlanIP clientNodeNum}")
        '';
      };
    virtual = {
      name = "Virtual";
      nodes.machine = {
        networking.useNetworkd = networkd;
        networking.useDHCP = false;
        networking.interfaces.tap0 = {
          ipv4.addresses = [
            {
              address = "192.168.1.1";
              prefixLength = 24;
            }
          ];
          ipv6.addresses = [
            {
              address = "2001:1470:fffd:2096::";
              prefixLength = 64;
            }
          ];
          virtual = true;
          virtualOwner = null;
          mtu = 1342;
          macAddress = "02:de:ad:be:ef:01";
        };
        networking.interfaces.tun0 = {
          ipv4.addresses = [
            {
              address = "192.168.1.2";
              prefixLength = 24;
            }
          ];
          ipv6.addresses = [
            {
              address = "2001:1470:fffd:2097::";
              prefixLength = 64;
            }
          ];
          virtual = true;
          virtualOwner = "root";
          mtu = 1343;
        };
      };

      testScript = ''
        targetList = """
        tap0: tap persist
        tun0: tun persist user 0
        """.strip()

        with subtest("Wait for networking to come up"):
            machine.start()
            machine.wait_for_unit("network.target")

        with subtest("Test interfaces set up"):
            list = machine.succeed("ip tuntap list | sort").strip()
            assert (
                list == targetList
            ), """
            The list of virtual interfaces does not match the expected one:
            Result:
              {}
            Expected:
              {}
            """.format(
                list, targetList
            )
        with subtest("Test MTU and MAC Address are configured"):
            machine.wait_until_succeeds("ip link show dev tap0 | grep 'mtu 1342'")
            machine.wait_until_succeeds("ip link show dev tun0 | grep 'mtu 1343'")
            assert "02:de:ad:be:ef:01" in machine.succeed("ip link show dev tap0")
        with subtest("Test virtualOwner are configured"):
            for interface, expected_owner in [("tap0", "-1"), ("tun0", "0")]:
                actual_owner = machine.succeed(f"cat /sys/class/net/{interface}/owner").strip()
                assert expected_owner == actual_owner, f"{interface} owner: expect {expected_owner}, got {actual_owner}"
      '' # network-addresses-* only exist in scripted networking
      + lib.optionalString (!networkd) ''
        with subtest("Test interfaces' addresses clean up"):
            machine.succeed("systemctl stop network-addresses-tap0")
            machine.sleep(10)
            machine.succeed("systemctl stop network-addresses-tun0")
            machine.sleep(10)
            residue = machine.succeed("ip tuntap list | sort").strip()
            assert (
                residue == targetList
            ), "Some virtual interface has been removed:\n{}".format(residue)
            assert "192.168.1.1" not in machine.succeed("ip address show dev tap0"), "tap0 interface address has not been removed"
            assert "192.168.1.2" not in machine.succeed("ip address show dev tun0"), "tun0 interface address has not been removed"

        with subtest("Test interfaces clean up"):
            machine.succeed("systemctl stop tap0-netdev")
            machine.sleep(10)
            machine.succeed("systemctl stop tun0-netdev")
            machine.sleep(10)
            residue = machine.succeed("ip tuntap list")
            assert (
                residue == ""
            ), "Some virtual interface has not been properly cleaned:\n{}".format(residue)
      '';
    };
    privacy = {
      name = "Privacy";
      nodes.router = {
        virtualisation.interfaces.enp1s0.vlan = 1;
        boot.kernel.sysctl."net.ipv6.conf.all.forwarding" = true;
        networking = {
          useNetworkd = networkd;
          useDHCP = false;
          interfaces.enp1s0.ipv6.addresses = lib.singleton {
            address = "fd00:1234:5678:1::1";
            prefixLength = 64;
          };
        };
        services.radvd = {
          enable = true;
          config = ''
            interface enp1s0 {
              AdvSendAdvert on;
              AdvManagedFlag on;
              AdvOtherConfigFlag on;

              prefix fd00:1234:5678:1::/64 {
                AdvAutonomous on;
                AdvOnLink on;
              };
            };
          '';
        };
      };
      nodes.client_with_privacy = {
        virtualisation.interfaces.enp1s0.vlan = 1;
        networking = {
          useNetworkd = networkd;
          useDHCP = false;
          interfaces.enp1s0 = {
            tempAddress = "default";
            ipv4.addresses = lib.mkOverride 0 [ ];
            ipv6.addresses = lib.mkOverride 0 [ ];
            useDHCP = true;
          };
        };
      };
      nodes.client = {
        virtualisation.interfaces.enp1s0.vlan = 1;
        networking = {
          useNetworkd = networkd;
          useDHCP = false;
          interfaces.enp1s0 = {
            tempAddress = "enabled";
            ipv4.addresses = lib.mkOverride 0 [ ];
            ipv6.addresses = lib.mkOverride 0 [ ];
            useDHCP = true;
          };
        };
      };
      testScript = ''
        start_all()

        client.wait_for_unit("network.target")
        client_with_privacy.wait_for_unit("network.target")
        router.systemctl("start network-online.target")
        router.wait_for_unit("network-online.target")

        with subtest("Wait until we have an ip address"):
            client_with_privacy.wait_until_succeeds(
                "ip addr show dev enp1s0 | 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")
            client.wait_until_succeeds("ping -c 1 fd00:1234:5678:1::1")

        with subtest("Test address used is temporary"):
            client_with_privacy.wait_until_succeeds(
                "! ip route get fd00:1234:5678:1::1 | grep -q ':[a-f0-9]*ff:fe[a-f0-9]*:'"
            )

        with subtest("Test address used is EUI-64"):
            client.wait_until_succeeds(
                "ip route get fd00:1234:5678:1::1 | grep -q ':[a-f0-9]*ff:fe[a-f0-9]*:'"
            )
      '';
    };
    routes = {
      name = "routes";
      nodes.machine = {
        networking.useNetworkd = networkd;
        networking.useDHCP = false;
        networking.interfaces.eth0 = {
          ipv4.addresses = [
            {
              address = "192.168.1.2";
              prefixLength = 24;
            }
          ];
          ipv6.addresses = [
            {
              address = "2001:1470:fffd:2097::";
              prefixLength = 64;
            }
          ];
          ipv6.routes = [
            {
              address = "fdfd:b3f0::";
              prefixLength = 48;
            }
            {
              address = "2001:1470:fffd:2098::";
              prefixLength = 64;
              via = "fdfd:b3f0::1";
            }
          ];
          ipv4.routes = [
            {
              address = "10.0.0.0";
              prefixLength = 16;
              options = {
                mtu = "1500";
                # Explicitly set scope because iproute and systemd-networkd
                # disagree on what the scope should be
                # if the type is the default "unicast"
                scope = "link";
              };
            }
            {
              address = "192.168.2.0";
              prefixLength = 24;
              via = "192.168.1.1";
            }
          ];
        };
        virtualisation.vlans = [ ];
      };

      testScript = ''
        targetIPv4Table = [
            "10.0.0.0/16 proto static scope link mtu 1500",
            "192.168.1.0/24 proto kernel scope link src 192.168.1.2",
            "192.168.2.0/24 via 192.168.1.1 proto static",
        ]

        targetIPv6Table = [
            "2001:1470:fffd:2097::/64 proto kernel metric 256 pref medium",
            "2001:1470:fffd:2098::/64 via fdfd:b3f0::1 proto static metric 1024 pref medium",
            "fdfd:b3f0::/48 proto static metric 1024 pref medium",
        ]

        machine.start()
        machine.wait_for_unit("network.target")

        with subtest("test routing tables"):
            ipv4Table = machine.succeed("ip -4 route list dev eth0 | head -n3").strip()
            ipv6Table = machine.succeed("ip -6 route list dev eth0 | head -n3").strip()
            assert [
                l.strip() for l in ipv4Table.splitlines()
            ] == targetIPv4Table, """
              The IPv4 routing table does not match the expected one:
                Result:
                  {}
                Expected:
                  {}
              """.format(
                ipv4Table, targetIPv4Table
            )
            assert [
                l.strip() for l in ipv6Table.splitlines()
            ] == targetIPv6Table, """
              The IPv6 routing table does not match the expected one:
                Result:
                  {}
                Expected:
                  {}
              """.format(
                ipv6Table, targetIPv6Table
            )

      ''
      + lib.optionalString (!networkd) ''
        with subtest("test clean-up of the tables"):
            machine.succeed("systemctl stop network-addresses-eth0")
            ipv4Residue = machine.succeed("ip -4 route list dev eth0 | head -n-3").strip()
            ipv6Residue = machine.succeed("ip -6 route list dev eth0 | head -n-3").strip()
            assert (
                ipv4Residue == ""
            ), "The IPv4 routing table has not been properly cleaned:\n{}".format(ipv4Residue)
            assert (
                ipv6Residue == ""
            ), "The IPv6 routing table has not been properly cleaned:\n{}".format(ipv6Residue)
      '';
    };
    rename =
      if networkd then
        {
          name = "RenameInterface";
          nodes.machine = {
            virtualisation.vlans = [ 1 ];
            networking = {
              useNetworkd = networkd;
              useDHCP = false;
            };
            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 = {
      name = "Link";
      nodes.client = {
        virtualisation.vlans = [ 1 ];
        networking = {
          useNetworkd = networkd;
          useDHCP = false;
        };
        systemd.network.links."50-foo" = {
          matchConfig = {
            Name = "foo";
            Driver = "dummy";
          };
          linkConfig.MTUBytes = "1442";
        };
      };
      testScript = ''
        print(client.succeed("ip l add name foo type dummy"))
        print(client.succeed("stat /etc/systemd/network/50-foo.link"))
        client.succeed("udevadm settle")
        assert "mtu 1442" in client.succeed("ip l show dev foo")
      '';
    };
    wlanInterface =
      let
        testMac = "06:00:00:00:02:00";
      in
      {
        name = "WlanInterface";
        nodes.machine = {
          boot.kernelModules = [ "mac80211_hwsim" ];
          networking.wlanInterfaces = {
            wlan0 = {
              device = "wlan0";
            };
            wap0 = {
              device = "wlan0";
              mac = testMac;
            };
          };
        };
        testScript = ''
          machine.start()
          machine.wait_for_unit("network.target")
          machine.wait_until_succeeds("ip address show wap0 | grep -q ${testMac}")
          machine.fail("ip address show wlan0 | grep -q ${testMac}")
        '';
      };
    naughtyInterfaceNames =
      let
        ifnames = [
          # flags of ip-address
          "home"
          "temporary"
          "optimistic"
          "bridge_slave"
          "flush"
          # flags of ip-route
          "up"
          "type"
          "nomaster"
          "address"
          # other
          "very_loong_name"
          "lowerUpper"
          "-"
        ];
      in
      {
        name = "naughtyInterfaceNames";
        nodes.machine = {
          networking.useNetworkd = networkd;
          networking.bridges = lib.listToAttrs (
            lib.flip map ifnames (name: {
              inherit name;
              value.interfaces = [ ];
            })
          );
        };
        testScript = ''
          machine.start()
          machine.wait_for_unit("network.target")
          for ifname in ${builtins.toJSON ifnames}:
              machine.wait_until_succeeds(f"ip link show dev '{ifname}' | grep -q '{ifname}'")
        '';
      };
    caseSensitiveRenaming = {
      name = "CaseSensitiveRenaming";
      nodes.machine = {
        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
lib.mapAttrs (lib.const (
  attrs:
  makeTest (
    attrs
    // {
      name = "${attrs.name}-Networking-${if networkd then "Networkd" else "Scripted"}";
      meta.maintainers = with lib.maintainers; [ rnhmjoj ];
    }
  )
)) testCases
