diff --git a/nixos/modules/config/networking.nix b/nixos/modules/config/networking.nix index 136a5bda7459..773d0b1f1a7d 100644 --- a/nixos/modules/config/networking.nix +++ b/nixos/modules/config/networking.nix @@ -84,13 +84,40 @@ in dnsmasq_conf=/etc/dnsmasq-conf.conf dnsmasq_resolv=/etc/dnsmasq-resolv.conf ''; - }; + + } // (optionalAttrs config.services.resolved.enable ( + if dnsmasqResolve then { + "dnsmasq-resolv.conf".source = "/run/systemd/resolve/resolv.conf"; + } else { + "resolv.conf".source = "/run/systemd/resolve/resolv.conf"; + } + )); # The ‘ip-up’ target is started when we have IP connectivity. So # services that depend on IP connectivity (like ntpd) should be # pulled in by this target. systemd.targets.ip-up.description = "Services Requiring IP Connectivity"; + # This is needed when /etc/resolv.conf is being overriden by networkd + # and other configurations. If the file is destroyed by an environment + # activation then it must be rebuilt so that applications which interface + # with /etc/resolv.conf directly don't break. + system.activationScripts.resolvconf = stringAfter [ "etc" "tmpfs" "var" ] + '' + # Systemd resolved controls its own resolv.conf + rm -f /run/resolvconf/interfaces/systemd + ${optionalString config.services.resolved.enable '' + rm -rf /run/resolvconf/interfaces + mkdir -p /run/resolvconf/interfaces + ln -s /run/systemd/resolve/resolv.conf /run/resolvconf/interfaces/systemd + ''} + + # Make sure resolv.conf is up to date if not managed by systemd + ${optionalString (!config.services.resolved.enable) '' + ${pkgs.openresolv}/bin/resolvconf -u + ''} + ''; + }; } diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index 8cdc9d2dd4c1..ecf68136f97b 100755 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -388,6 +388,8 @@ ./tasks/kbd.nix ./tasks/lvm.nix ./tasks/network-interfaces.nix + ./tasks/network-interfaces-systemd.nix + ./tasks/network-interfaces-scripted.nix ./tasks/scsi-link-power-management.nix ./tasks/swraid.nix ./tasks/trackpoint.nix diff --git a/nixos/modules/services/networking/chrony.nix b/nixos/modules/services/networking/chrony.nix index 58b30269ca7a..fe062b30e4b7 100644 --- a/nixos/modules/services/networking/chrony.nix +++ b/nixos/modules/services/networking/chrony.nix @@ -100,8 +100,8 @@ in jobs.chronyd = { description = "chrony daemon"; - wantedBy = [ "ip-up.target" ]; - partOf = [ "ip-up.target" ]; + wantedBy = [ "multi-user.target" ]; + after = [ "network.target" ]; path = [ chrony ]; diff --git a/nixos/modules/services/networking/dhcpcd.nix b/nixos/modules/services/networking/dhcpcd.nix index 15dbf80a987e..1ad8cbae15cf 100644 --- a/nixos/modules/services/networking/dhcpcd.nix +++ b/nixos/modules/services/networking/dhcpcd.nix @@ -8,15 +8,29 @@ let cfg = config.networking.dhcpcd; + interfaces = attrValues config.networking.interfaces; + + enableDHCP = config.networking.useDHCP || any (i: i.useDHCP == true) interfaces; + # Don't start dhcpcd on explicitly configured interfaces or on # interfaces that are part of a bridge, bond or sit device. ignoredInterfaces = - map (i: i.name) (filter (i: i.ip4 != [ ] || i.ipAddress != null) (attrValues config.networking.interfaces)) + map (i: i.name) (filter (i: if i.useDHCP != null then !i.useDHCP else i.ip4 != [ ] || i.ipAddress != null) interfaces) ++ mapAttrsToList (i: _: i) config.networking.sits ++ concatLists (attrValues (mapAttrs (n: v: v.interfaces) config.networking.bridges)) ++ concatLists (attrValues (mapAttrs (n: v: v.interfaces) config.networking.bonds)) ++ config.networking.dhcpcd.denyInterfaces; + arrayAppendOrNull = a1: a2: if a1 == null && a2 == null then null + else if a1 == null then a2 else if a2 == null then a1 + else a1 ++ a2; + + # If dhcp is disabled but explicit interfaces are enabled, + # we need to provide dhcp just for those interfaces. + allowInterfaces = arrayAppendOrNull cfg.allowInterfaces + (if !config.networking.useDHCP && enableDHCP then + map (i: i.name) (filter (i: i.useDHCP == true) interfaces) else null); + # Config file adapted from the one that ships with dhcpcd. dhcpcdConf = pkgs.writeText "dhcpcd.conf" '' @@ -41,7 +55,7 @@ let denyinterfaces ${toString ignoredInterfaces} lo peth* vif* tap* tun* virbr* vnet* vboxnet* sit* # Use the list of allowed interfaces if specified - ${optionalString (cfg.allowInterfaces != null) "allowinterfaces ${toString cfg.allowInterfaces}"} + ${optionalString (allowInterfaces != null) "allowinterfaces ${toString allowInterfaces}"} ${cfg.extraConfig} ''; @@ -132,7 +146,7 @@ in ###### implementation - config = mkIf config.networking.useDHCP { + config = mkIf enableDHCP { systemd.services.dhcpcd = { description = "DHCP Client"; diff --git a/nixos/modules/services/networking/dnsmasq.nix b/nixos/modules/services/networking/dnsmasq.nix index 5c68dd89fb12..fbb211911f1c 100644 --- a/nixos/modules/services/networking/dnsmasq.nix +++ b/nixos/modules/services/networking/dnsmasq.nix @@ -82,7 +82,7 @@ in systemd.services.dnsmasq = { description = "dnsmasq daemon"; - after = [ "network.target" ]; + after = [ "network.target" "systemd-resolved.conf" ]; wantedBy = [ "multi-user.target" ]; path = [ dnsmasq ]; preStart = '' diff --git a/nixos/modules/services/networking/gogoclient.nix b/nixos/modules/services/networking/gogoclient.nix index 416007941976..9d16f0efb435 100644 --- a/nixos/modules/services/networking/gogoclient.nix +++ b/nixos/modules/services/networking/gogoclient.nix @@ -76,8 +76,7 @@ in exec ${pkgs.gogoclient}/bin/gogoc -y -f /var/lib/gogoc/gogoc.conf ''; } // optionalAttrs cfg.autorun { - wantedBy = [ "ip-up.target" ]; - partOf = [ "ip-up.target" ]; + wantedBy = [ "multi-user.target" ]; }; }; diff --git a/nixos/modules/services/networking/networkmanager.nix b/nixos/modules/services/networking/networkmanager.nix index 39e83e7b4272..37bc1df2361b 100644 --- a/nixos/modules/services/networking/networkmanager.nix +++ b/nixos/modules/services/networking/networkmanager.nix @@ -52,6 +52,7 @@ let #!/bin/sh if test "$2" = "up"; then ${config.systemd.package}/bin/systemctl start ip-up.target + ${config.systemd.package}/bin/systemctl start network-online.target fi ''; diff --git a/nixos/modules/services/networking/ntpd.nix b/nixos/modules/services/networking/ntpd.nix index 527882aad288..8f4bf26d411d 100644 --- a/nixos/modules/services/networking/ntpd.nix +++ b/nixos/modules/services/networking/ntpd.nix @@ -77,8 +77,7 @@ in jobs.ntpd = { description = "NTP Daemon"; - wantedBy = [ "ip-up.target" ]; - partOf = [ "ip-up.target" ]; + wantedBy = [ "multi-user.target" ]; path = [ ntp ]; diff --git a/nixos/modules/services/networking/openntpd.nix b/nixos/modules/services/networking/openntpd.nix index bd8a7a04a2af..2f9031481d1d 100644 --- a/nixos/modules/services/networking/openntpd.nix +++ b/nixos/modules/services/networking/openntpd.nix @@ -41,8 +41,7 @@ in systemd.services.openntpd = { description = "OpenNTP Server"; - wantedBy = [ "ip-up.target" ]; - partOf = [ "ip-up.target" ]; + wantedBy = [ "multi-user.target" ]; serviceConfig.ExecStart = "${package}/sbin/ntpd -d -f ${cfgFile}"; }; }; diff --git a/nixos/modules/system/boot/stage-2-init.sh b/nixos/modules/system/boot/stage-2-init.sh index 6fff776f8581..3762bda94a5c 100644 --- a/nixos/modules/system/boot/stage-2-init.sh +++ b/nixos/modules/system/boot/stage-2-init.sh @@ -141,8 +141,6 @@ fi # Use /etc/resolv.conf supplied by systemd-nspawn, if applicable. if [ -n "@useHostResolvConf@" -a -e /etc/resolv.conf ]; then cat /etc/resolv.conf | resolvconf -m 1000 -a host -else - touch /etc/resolv.conf fi diff --git a/nixos/modules/system/boot/systemd-unit-options.nix b/nixos/modules/system/boot/systemd-unit-options.nix index 07f3cb9e952c..20851c626d75 100644 --- a/nixos/modules/system/boot/systemd-unit-options.nix +++ b/nixos/modules/system/boot/systemd-unit-options.nix @@ -4,15 +4,184 @@ with lib; let - checkService = v: - let assertValueOneOf = name: values: attr: - let val = attr.${name}; - in optional (attr ? ${name} && !elem val values) "Systemd service field `${name}' cannot have value `${val}'."; - checkType = assertValueOneOf "Type" ["simple" "forking" "oneshot" "dbus" "notify" "idle"]; - checkRestart = assertValueOneOf "Restart" ["no" "on-success" "on-failure" "on-abort" "always"]; - errors = concatMap (c: c v) [checkType checkRestart]; - in if errors == [] then true - else builtins.trace (concatStringsSep "\n" errors) false; + boolValues = [true false "yes" "no"]; + + assertValueOneOf = name: values: group: attr: + optional (attr ? ${name} && !elem attr.${name} values) + "Systemd ${group} field `${name}' cannot have value `${attr.${name}}'."; + + assertHasField = name: group: attr: + optional (!(attr ? ${name})) + "Systemd ${group} field `${name}' must exist."; + + assertOnlyFields = fields: group: attr: + let badFields = filter (name: ! elem name fields) (attrNames attr); in + optional (badFields != [ ]) + "Systemd ${group} has extra fields [${concatStringsSep " " badFields}]."; + + assertRange = name: min: max: group: attr: + optional (attr ? ${name} && !(min <= attr.${name} && max >= attr.${name})) + "Systemd ${group} field `${name}' is outside the range [${toString min},${toString max}]"; + + digits = map toString (range 0 9); + + isByteFormat = s: + let + l = reverseList (stringToCharacters s); + suffix = head l; + nums = tail l; + in elem suffix (["K" "M" "G" "T"] ++ digits) + && all (num: elem num digits) nums; + + assertByteFormat = name: group: attr: + optional (attr ? ${name} && ! isByteFormat attr.${name}) + "Systemd ${group} field `${name}' must be in byte format [0-9]+[KMGT]."; + + hexChars = stringToCharacters "0123456789abcdefABCDEF"; + + isMacAddress = s: stringLength s == 17 + && flip all (splitString ":" s) (bytes: + all (byte: elem byte hexChars) (stringToCharacters bytes) + ); + + assertMacAddress = name: group: attr: + optional (attr ? ${name} && ! isMacAddress attr.${name}) + "Systemd ${group} field `${name}' must be a valid mac address."; + + checkUnitConfig = group: checks: v: + let errors = concatMap (c: c group v) checks; in + if errors == [] then true + else builtins.trace (concatStringsSep "\n" errors) false; + + checkService = checkUnitConfig "Service" [ + (assertValueOneOf "Type" [ + "simple" "forking" "oneshot" "dbus" "notify" "idle" + ]) + (assertValueOneOf "Restart" [ + "no" "on-success" "on-failure" "on-abort" "always" + ]) + ]; + + checkLink = checkUnitConfig "Link" [ + (assertOnlyFields [ + "Description" "Alias" "MACAddressPolicy" "MACAddress" "NamePolicy" "Name" + "MTUBytes" "BitsPerSecond" "Duplex" "WakeOnLan" + ]) + (assertValueOneOf "MACAddressPolicy" ["persistent" "random"]) + (assertMacAddress "MACAddress") + (assertValueOneOf "NamePolicy" [ + "kernel" "database" "onboard" "slot" "path" "mac" + ]) + (assertByteFormat "MTUBytes") + (assertByteFormat "BitsPerSecond") + (assertValueOneOf "Duplex" ["half" "full"]) + (assertValueOneOf "WakeOnLan" ["phy" "magic" "off"]) + ]; + + checkNetdev = checkUnitConfig "Netdev" [ + (assertOnlyFields [ + "Description" "Name" "Kind" "MTUBytes" "MACAddress" + ]) + (assertHasField "Name") + (assertHasField "Kind") + (assertValueOneOf "Kind" [ + "bridge" "bond" "vlan" "macvlan" "vxlan" "ipip" + "gre" "sit" "vti" "veth" "tun" "tap" "dummy" + ]) + (assertByteFormat "MTUBytes") + (assertMacAddress "MACAddress") + ]; + + checkVlan = checkUnitConfig "VLAN" [ + (assertOnlyFields ["Id"]) + (assertRange "Id" 0 4094) + ]; + + checkMacvlan = checkUnitConfig "MACVLAN" [ + (assertOnlyFields ["Mode"]) + (assertValueOneOf "Mode" ["private" "vepa" "bridge" "passthru"]) + ]; + + checkVxlan = checkUnitConfig "VXLAN" [ + (assertOnlyFields ["Id" "Group" "TOS" "TTL" "MacLearning"]) + (assertRange "TTL" 0 255) + (assertValueOneOf "MacLearning" boolValues) + ]; + + checkTunnel = checkUnitConfig "Tunnel" [ + (assertOnlyFields ["Local" "Remote" "TOS" "TTL" "DiscoverPathMTU"]) + (assertRange "TTL" 0 255) + (assertValueOneOf "DiscoverPathMTU" boolValues) + ]; + + checkPeer = checkUnitConfig "Peer" [ + (assertOnlyFields ["Name" "MACAddress"]) + (assertMacAddress "MACAddress") + ]; + + tunTapChecks = [ + (assertOnlyFields ["OneQueue" "MultiQueue" "PacketInfo" "User" "Group"]) + (assertValueOneOf "OneQueue" boolValues) + (assertValueOneOf "MultiQueue" boolValues) + (assertValueOneOf "PacketInfo" boolValues) + ]; + + checkTun = checkUnitConfig "Tun" tunTapChecks; + + checkTap = checkUnitConfig "Tap" tunTapChecks; + + checkBond = checkUnitConfig "Bond" [ + (assertOnlyFields [ + "Mode" "TransmitHashPolicy" "LACPTransmitRate" "MIIMonitorSec" + "UpDelaySec" "DownDelaySec" + ]) + (assertValueOneOf "Mode" [ + "balance-rr" "active-backup" "balance-xor" + "broadcast" "802.3ad" "balance-tlb" "balance-alb" + ]) + (assertValueOneOf "TransmitHashPolicy" [ + "layer2" "layer3+4" "layer2+3" "encap2+3" "802.3ad" "encap3+4" + ]) + (assertValueOneOf "LACPTransmitRate" ["slow" "fast"]) + ]; + + checkNetwork = checkUnitConfig "Network" [ + (assertOnlyFields [ + "Description" "DHCP" "DHCPServer" "IPv4LL" "IPv4LLRoute" + "LLMNR" "Domains" "Bridge" "Bond" + ]) + (assertValueOneOf "DHCP" ["both" "none" "v4" "v6"]) + (assertValueOneOf "DHCPServer" boolValues) + (assertValueOneOf "IPv4LL" boolValues) + (assertValueOneOf "IPv4LLRoute" boolValues) + (assertValueOneOf "LLMNR" boolValues) + ]; + + checkAddress = checkUnitConfig "Address" [ + (assertOnlyFields ["Address" "Peer" "Broadcast" "Label"]) + (assertHasField "Address") + ]; + + checkRoute = checkUnitConfig "Route" [ + (assertOnlyFields ["Gateway" "Destination" "Metric"]) + (assertHasField "Gateway") + ]; + + checkDhcp = checkUnitConfig "DHCP" [ + (assertOnlyFields [ + "UseDNS" "UseMTU" "SendHostname" "UseHostname" "UseDomains" "UseRoutes" + "CriticalConnections" "VendorClassIdentifier" "RequestBroadcast" + "RouteMetric" + ]) + (assertValueOneOf "UseDNS" boolValues) + (assertValueOneOf "UseMTU" boolValues) + (assertValueOneOf "SendHostname" boolValues) + (assertValueOneOf "UseHostname" boolValues) + (assertValueOneOf "UseDomains" boolValues) + (assertValueOneOf "UseRoutes" boolValues) + (assertValueOneOf "CriticalConnections" boolValues) + (assertValueOneOf "RequestBroadcast" boolValues) + ]; unitOption = mkOptionType { name = "systemd option"; @@ -140,6 +309,15 @@ in rec { ''; }; + requisite = mkOption { + default = []; + type = types.listOf types.str; + description = '' + Similar to requires. However if the units listed are not started, + they will not be started and the transaction will fail. + ''; + }; + unitConfig = mkOption { default = {}; example = { RequiresMountsFor = "/data"; }; @@ -441,4 +619,345 @@ in rec { targetOptions = commonUnitOptions; + commonNetworkOptions = { + + enable = mkOption { + default = true; + type = types.bool; + description = '' + If set to false, this unit will be a symlink to + /dev/null. + ''; + }; + + matchConfig = mkOption { + default = {}; + example = { Name = "eth0"; }; + type = types.attrsOf unitOption; + description = '' + Each attribute in this set specifies an option in the + [Match] section of the unit. See + systemd.link5 + systemd.netdev5 + systemd.network5 + for details. + ''; + }; + + }; + + linkOptions = commonNetworkOptions // { + + linkConfig = mkOption { + default = {}; + example = { MACAddress = "00:ff:ee:aa:cc:dd"; }; + type = types.addCheck (types.attrsOf unitOption) checkLink; + description = '' + Each attribute in this set specifies an option in the + [Link] section of the unit. See + systemd.link + 5 for details. + ''; + }; + + }; + + netdevOptions = commonNetworkOptions // { + + netdevConfig = mkOption { + default = {}; + example = { Name = "mybridge"; Kind = "bridge"; }; + type = types.addCheck (types.attrsOf unitOption) checkNetdev; + description = '' + Each attribute in this set specifies an option in the + [Netdev] section of the unit. See + systemd.netdev + 5 for details. + ''; + }; + + vlanConfig = mkOption { + default = {}; + example = { Id = "4"; }; + type = types.addCheck (types.attrsOf unitOption) checkVlan; + description = '' + Each attribute in this set specifies an option in the + [VLAN] section of the unit. See + systemd.netdev + 5 for details. + ''; + }; + + macvlanConfig = mkOption { + default = {}; + example = { Mode = "private"; }; + type = types.addCheck (types.attrsOf unitOption) checkMacvlan; + description = '' + Each attribute in this set specifies an option in the + [MACVLAN] section of the unit. See + systemd.netdev + 5 for details. + ''; + }; + + vxlanConfig = mkOption { + default = {}; + example = { Id = "4"; }; + type = types.addCheck (types.attrsOf unitOption) checkVxlan; + description = '' + Each attribute in this set specifies an option in the + [VXLAN] section of the unit. See + systemd.netdev + 5 for details. + ''; + }; + + tunnelConfig = mkOption { + default = {}; + example = { Remote = "192.168.1.1"; }; + type = types.addCheck (types.attrsOf unitOption) checkTunnel; + description = '' + Each attribute in this set specifies an option in the + [Tunnel] section of the unit. See + systemd.netdev + 5 for details. + ''; + }; + + peerConfig = mkOption { + default = {}; + example = { Name = "veth2"; }; + type = types.addCheck (types.attrsOf unitOption) checkPeer; + description = '' + Each attribute in this set specifies an option in the + [Peer] section of the unit. See + systemd.netdev + 5 for details. + ''; + }; + + tunConfig = mkOption { + default = {}; + example = { User = "openvpn"; }; + type = types.addCheck (types.attrsOf unitOption) checkTun; + description = '' + Each attribute in this set specifies an option in the + [Tun] section of the unit. See + systemd.netdev + 5 for details. + ''; + }; + + tapConfig = mkOption { + default = {}; + example = { User = "openvpn"; }; + type = types.addCheck (types.attrsOf unitOption) checkTap; + description = '' + Each attribute in this set specifies an option in the + [Tap] section of the unit. See + systemd.netdev + 5 for details. + ''; + }; + + bondConfig = mkOption { + default = {}; + example = { Mode = "802.3ad"; }; + type = types.addCheck (types.attrsOf unitOption) checkBond; + description = '' + Each attribute in this set specifies an option in the + [Bond] section of the unit. See + systemd.netdev + 5 for details. + ''; + }; + + }; + + addressOptions = { + + addressConfig = mkOption { + default = {}; + example = { Address = "192.168.0.100/24"; }; + type = types.addCheck (types.attrsOf unitOption) checkAddress; + description = '' + Each attribute in this set specifies an option in the + [Address] section of the unit. See + systemd.network + 5 for details. + ''; + }; + + }; + + routeOptions = { + + routeConfig = mkOption { + default = {}; + example = { Gateway = "192.168.0.1"; }; + type = types.addCheck (types.attrsOf unitOption) checkRoute; + description = '' + Each attribute in this set specifies an option in the + [Route] section of the unit. See + systemd.network + 5 for details. + ''; + }; + + }; + + networkOptions = commonNetworkOptions // { + + networkConfig = mkOption { + default = {}; + example = { Description = "My Network"; }; + type = types.addCheck (types.attrsOf unitOption) checkNetwork; + description = '' + Each attribute in this set specifies an option in the + [Network] section of the unit. See + systemd.network + 5 for details. + ''; + }; + + dhcpConfig = mkOption { + default = {}; + example = { UseDNS = true; UseRoutes = true; }; + type = types.addCheck (types.attrsOf unitOption) checkDhcp; + description = '' + Each attribute in this set specifies an option in the + [DHCP] section of the unit. See + systemd.network + 5 for details. + ''; + }; + + name = mkOption { + type = types.nullOr types.str; + default = null; + description = '' + The name of the network interface to match against. + ''; + }; + + DHCP = mkOption { + type = types.nullOr types.str; + default = null; + description = '' + Whether to enable DHCP on the interfaces matched. + ''; + }; + + domains = mkOption { + type = types.nullOr (types.listOf types.str); + default = null; + description = '' + A list of domains to pass to the network config. + ''; + }; + + address = mkOption { + default = [ ]; + type = types.listOf types.str; + description = '' + A list of addresses to be added to the network section of the + unit. See systemd.network + 5 for details. + ''; + }; + + gateway = mkOption { + default = [ ]; + type = types.listOf types.str; + description = '' + A list of gateways to be added to the network section of the + unit. See systemd.network + 5 for details. + ''; + }; + + dns = mkOption { + default = [ ]; + type = types.listOf types.str; + description = '' + A list of dns servers to be added to the network section of the + unit. See systemd.network + 5 for details. + ''; + }; + + ntp = mkOption { + default = [ ]; + type = types.listOf types.str; + description = '' + A list of ntp servers to be added to the network section of the + unit. See systemd.network + 5 for details. + ''; + }; + + vlan = mkOption { + default = [ ]; + type = types.listOf types.str; + description = '' + A list of vlan interfaces to be added to the network section of the + unit. See systemd.network + 5 for details. + ''; + }; + + macvlan = mkOption { + default = [ ]; + type = types.listOf types.str; + description = '' + A list of macvlan interfaces to be added to the network section of the + unit. See systemd.network + 5 for details. + ''; + }; + + vxlan = mkOption { + default = [ ]; + type = types.listOf types.str; + description = '' + A list of vxlan interfaces to be added to the network section of the + unit. See systemd.network + 5 for details. + ''; + }; + + tunnel = mkOption { + default = [ ]; + type = types.listOf types.str; + description = '' + A list of tunnel interfaces to be added to the network section of the + unit. See systemd.network + 5 for details. + ''; + }; + + addresses = mkOption { + default = [ ]; + type = types.listOf types.optionSet; + options = [ addressOptions ]; + description = '' + A list of address sections to be added to the unit. See + systemd.network + 5 for details. + ''; + }; + + routes = mkOption { + default = [ ]; + type = types.listOf types.optionSet; + options = [ routeOptions ]; + description = '' + A list of route sections to be added to the unit. See + systemd.network + 5 for details. + ''; + }; + + }; + } diff --git a/nixos/modules/system/boot/systemd.nix b/nixos/modules/system/boot/systemd.nix index 8a86149a9e12..89029a098e96 100644 --- a/nixos/modules/system/boot/systemd.nix +++ b/nixos/modules/system/boot/systemd.nix @@ -96,6 +96,12 @@ let "systemd-modules-load.service" "kmod-static-nodes.service" + # Networking + "systemd-networkd.service" + "systemd-networkd-wait-online.service" + "systemd-resolved.service" + "systemd-timesyncd.service" + # Filesystems. "systemd-fsck@.service" "systemd-fsck-root.service" @@ -212,6 +218,8 @@ let { PartOf = toString config.partOf; } // optionalAttrs (config.conflicts != []) { Conflicts = toString config.conflicts; } + // optionalAttrs (config.requisite != []) + { Requisite = toString config.requisite; } // optionalAttrs (config.restartTriggers != []) { X-Restart-Triggers = toString config.restartTriggers; } // optionalAttrs (config.description != "") { @@ -292,6 +300,19 @@ let }; }; + networkConfig = { name, config, ... }: { + config = { + matchConfig = optionalAttrs (config.name != null) { + Name = config.name; + }; + networkConfig = optionalAttrs (config.DHCP != null) { + DHCP = config.DHCP; + } // optionalAttrs (config.domains != null) { + Domains = concatStringsSep " " config.domains; + }; + }; + }; + toOption = x: if x == true then "true" else if x == false then "false" @@ -384,6 +405,103 @@ let ''; }; + commonMatchText = def: '' + [Match] + ${attrsToSection def.matchConfig} + ''; + + linkToUnit = name: def: + { inherit (def) enable; + text = commonMatchText def + + '' + [Link] + ${attrsToSection def.linkConfig} + ''; + }; + + netdevToUnit = name: def: + { inherit (def) enable; + text = commonMatchText def + + '' + [NetDev] + ${attrsToSection def.netdevConfig} + + ${optionalString (def.vlanConfig != { }) '' + [VLAN] + ${attrsToSection def.vlanConfig} + + ''} + ${optionalString (def.macvlanConfig != { }) '' + [MACVLAN] + ${attrsToSection def.macvlanConfig} + + ''} + ${optionalString (def.vxlanConfig != { }) '' + [VXLAN] + ${attrsToSection def.vxlanConfig} + + ''} + ${optionalString (def.tunnelConfig != { }) '' + [Tunnel] + ${attrsToSection def.tunnelConfig} + + ''} + ${optionalString (def.peerConfig != { }) '' + [Peer] + ${attrsToSection def.peerConfig} + + ''} + ${optionalString (def.tunConfig != { }) '' + [Tun] + ${attrsToSection def.tunConfig} + + ''} + ${optionalString (def.tapConfig != { }) '' + [Tap] + ${attrsToSection def.tapConfig} + + ''} + ${optionalString (def.bondConfig != { }) '' + [Bond] + ${attrsToSection def.bondConfig} + + ''} + ''; + }; + + networkToUnit = name: def: + { inherit (def) enable; + text = commonMatchText def + + '' + [Network] + ${attrsToSection def.networkConfig} + ${concatStringsSep "\n" (map (s: "Address=${s}") def.address)} + ${concatStringsSep "\n" (map (s: "Gateway=${s}") def.gateway)} + ${concatStringsSep "\n" (map (s: "DNS=${s}") def.dns)} + ${concatStringsSep "\n" (map (s: "NTP=${s}") def.ntp)} + ${concatStringsSep "\n" (map (s: "VLAN=${s}") def.vlan)} + ${concatStringsSep "\n" (map (s: "MACVLAN=${s}") def.macvlan)} + ${concatStringsSep "\n" (map (s: "VXLAN=${s}") def.vxlan)} + ${concatStringsSep "\n" (map (s: "Tunnel=${s}") def.tunnel)} + + ${optionalString (def.dhcpConfig != { }) '' + [DHCP] + ${attrsToSection def.dhcpConfig} + + ''} + ${flip concatMapStrings def.addresses (x: '' + [Address] + ${attrsToSection x.addressConfig} + + '')} + ${flip concatMapStrings def.routes (x: '' + [Route] + ${attrsToSection x.routeConfig} + + '')} + ''; + }; + generateUnits = type: units: upstreamUnits: upstreamWants: pkgs.runCommand "${type}-units" { preferLocalBuild = true; } '' mkdir -p $out @@ -468,8 +586,9 @@ let mkdir -p $out/getty.target.wants/ ln -s ../autovt@tty1.service $out/getty.target.wants/ - ln -s ../local-fs.target ../remote-fs.target ../network.target ../nss-lookup.target \ - ../nss-user-lookup.target ../swap.target $out/multi-user.target.wants/ + ln -s ../local-fs.target ../remote-fs.target ../network.target \ + ../nss-lookup.target ../nss-user-lookup.target ../swap.target \ + $out/multi-user.target.wants/ ''} ''; # */ @@ -562,6 +681,47 @@ in ''; }; + systemd.network.enable = mkOption { + default = false; + type = types.bool; + description = '' + Whether to enable networkd or not. + ''; + }; + + systemd.network.links = mkOption { + default = {}; + type = types.attrsOf types.optionSet; + options = [ linkOptions ]; + description = "Definiton of systemd network links."; + }; + + systemd.network.netdevs = mkOption { + default = {}; + type = types.attrsOf types.optionSet; + options = [ netdevOptions ]; + description = "Definiton of systemd network devices."; + }; + + systemd.network.networks = mkOption { + default = {}; + type = types.attrsOf types.optionSet; + options = [ networkOptions networkConfig ]; + description = "Definiton of systemd networks."; + }; + + systemd.network.units = mkOption { + description = "Definition of networkd units."; + default = {}; + type = types.attrsOf types.optionSet; + options = { name, config, ... }: + { options = concreteUnitOptions; + config = { + unit = mkDefault (makeUnit name config); + }; + }; + }; + systemd.defaultUnit = mkOption { default = "multi-user.target"; type = types.str; @@ -645,6 +805,22 @@ in ''; }; + services.resolved.enable = mkOption { + default = false; + type = types.bool; + description = '' + Enables the systemd dns resolver daemon. + ''; + }; + + services.timesyncd.enable = mkOption { + default = false; + type = types.bool; + description = '' + Enables the systemd ntp client daemon. + ''; + }; + systemd.tmpfiles.rules = mkOption { type = types.listOf types.str; default = []; @@ -701,7 +877,7 @@ in ###### implementation - config = { + config = mkMerge [ { warnings = concatLists (mapAttrsToList (name: service: optional (service.serviceConfig.Type or "" == "oneshot" && service.serviceConfig.Restart or "no" != "no") @@ -714,6 +890,9 @@ in environment.etc."systemd/system".source = generateUnits "system" cfg.units upstreamSystemUnits upstreamSystemWants; + environment.etc."systemd/network".source = + generateUnits "network" cfg.network.units [] []; + environment.etc."systemd/user".source = generateUnits "user" cfg.user.units upstreamUserUnits []; @@ -766,6 +945,8 @@ in unitConfig.X-StopOnReconfiguration = true; }; + systemd.targets.network-online.after = [ "ip-up.target" ]; + systemd.units = mapAttrs' (n: v: nameValuePair "${n}.target" (targetToUnit n v)) cfg.targets // mapAttrs' (n: v: nameValuePair "${n}.service" (serviceToUnit n v)) cfg.services @@ -779,6 +960,11 @@ in (v: let n = escapeSystemdPath v.where; in nameValuePair "${n}.automount" (automountToUnit n v)) cfg.automounts); + systemd.network.units = + mapAttrs' (n: v: nameValuePair "${n}.link" (linkToUnit n v)) cfg.network.links + // mapAttrs' (n: v: nameValuePair "${n}.netdev" (netdevToUnit n v)) cfg.network.netdevs + // mapAttrs' (n: v: nameValuePair "${n}.network" (networkToUnit n v)) cfg.network.networks; + systemd.user.units = mapAttrs' (n: v: nameValuePair "${n}.service" (serviceToUnit n v)) cfg.user.services // mapAttrs' (n: v: nameValuePair "${n}.socket" (socketToUnit n v)) cfg.user.sockets; @@ -833,5 +1019,65 @@ in systemd.services.systemd-remount-fs.restartIfChanged = false; systemd.services.systemd-journal-flush.restartIfChanged = false; - }; + } + (mkIf config.systemd.network.enable { + users.extraUsers.systemd-network.uid = config.ids.uids.systemd-network; + users.extraGroups.systemd-network.gid = config.ids.gids.systemd-network; + + systemd.services.systemd-networkd = { + wantedBy = [ "multi-user.target" ]; + restartTriggers = [ config.environment.etc."systemd/network".source ]; + }; + + systemd.services.systemd-networkd-wait-online = { + before = [ "network-online.target" "ip-up.target" ]; + wantedBy = [ "network-online.target" "ip-up.target" ]; + }; + + systemd.services."systemd-network-wait-online@" = { + description = "Wait for Network Interface %I to be Configured"; + conflicts = [ "shutdown.target" ]; + requisite = [ "systemd-networkd.service" ]; + after = [ "systemd-networkd.service" ]; + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + ExecStart = "${config.systemd.package}/lib/systemd/systemd-networkd-wait-online -i %I"; + }; + }; + + services.resolved.enable = mkDefault true; + services.timesyncd.enable = mkDefault config.services.ntp.enable; + }) + (mkIf config.services.resolved.enable { + users.extraUsers.systemd-resolve.uid = config.ids.uids.systemd-resolve; + users.extraGroups.systemd-resolve.gid = config.ids.gids.systemd-resolve; + + systemd.services.systemd-resolved = { + wantedBy = [ "multi-user.target" ]; + restartTriggers = [ config.environment.etc."systemd/resolved.conf".source ]; + }; + + environment.etc."systemd/resolved.conf".text = '' + [Resolve] + DNS=${concatStringsSep " " config.networking.nameservers} + ''; + }) + (mkIf config.services.timesyncd.enable { + users.extraUsers.systemd-timesync.uid = config.ids.uids.systemd-timesync; + users.extraGroups.systemd-timesync.gid = config.ids.gids.systemd-timesync; + + systemd.services.systemd-timesyncd = { + wantedBy = [ "sysinit.target" ]; + restartTriggers = [ config.environment.etc."systemd/timesyncd.conf".source ]; + }; + + environment.etc."systemd/timesyncd.conf".text = '' + [Time] + NTP=${concatStringsSep " " config.services.ntp.servers} + ''; + + systemd.services.ntpd.enable = false; + }) + ]; } diff --git a/nixos/modules/tasks/filesystems/nfs.nix b/nixos/modules/tasks/filesystems/nfs.nix index 16752ce7e1b5..f7f2d9658069 100644 --- a/nixos/modules/tasks/filesystems/nfs.nix +++ b/nixos/modules/tasks/filesystems/nfs.nix @@ -73,8 +73,7 @@ in path = [ pkgs.nfsUtils pkgs.sysvtools pkgs.utillinux ]; - wantedBy = [ "network-online.target" "multi-user.target" ]; - before = [ "network-online.target" ]; + wantedBy = [ "multi-user.target" ]; requires = [ "basic.target" "rpcbind.service" ]; after = [ "basic.target" "rpcbind.service" "network.target" ]; @@ -100,8 +99,7 @@ in path = [ pkgs.sysvtools pkgs.utillinux ]; - wantedBy = [ "network-online.target" "multi-user.target" ]; - before = [ "network-online.target" ]; + wantedBy = [ "multi-user.target" ]; requires = [ "rpcbind.service" ]; after = [ "rpcbind.service" ]; diff --git a/nixos/modules/tasks/network-interfaces-scripted.nix b/nixos/modules/tasks/network-interfaces-scripted.nix new file mode 100644 index 000000000000..30fcb3a80104 --- /dev/null +++ b/nixos/modules/tasks/network-interfaces-scripted.nix @@ -0,0 +1,340 @@ +{ config, lib, pkgs, utils, ... }: + +with lib; +with utils; + +let + + cfg = config.networking; + interfaces = attrValues cfg.interfaces; + hasVirtuals = any (i: i.virtual) interfaces; + + # We must escape interfaces due to the systemd interpretation + subsystemDevice = interface: + "sys-subsystem-net-devices-${escapeSystemdPath interface}.device"; + + interfaceIps = i: + i.ip4 ++ optionals cfg.enableIPv6 i.ip6 + ++ optional (i.ipAddress != null) { + address = i.ipAddress; + prefixLength = i.prefixLength; + } ++ optional (cfg.enableIPv6 && i.ipv6Address != null) { + address = i.ipv6Address; + prefixLength = i.ipv6PrefixLength; + }; + + destroyBond = i: '' + while true; do + UPDATED=1 + SLAVES=$(ip link | grep 'master ${i}' | awk -F: '{print $2}') + for I in $SLAVES; do + UPDATED=0 + ip link set "$I" nomaster + done + [ "$UPDATED" -eq "1" ] && break + done + ip link set "${i}" down || true + ip link del "${i}" || true + ''; + +in + +{ + + config = mkIf (!cfg.useNetworkd) { + + systemd.targets."network-interfaces" = + { description = "All Network Interfaces"; + wantedBy = [ "network.target" ]; + unitConfig.X-StopOnReconfiguration = true; + }; + + systemd.services = + let + + networkLocalCommands = { + after = [ "network-setup.service" ]; + bindsTo = [ "network-setup.service" ]; + }; + + networkSetup = + { description = "Networking Setup"; + + after = [ "network-interfaces.target" ]; + before = [ "network.target" ]; + wantedBy = [ "network.target" ]; + + unitConfig.ConditionCapability = "CAP_NET_ADMIN"; + + path = [ pkgs.iproute ]; + + serviceConfig.Type = "oneshot"; + serviceConfig.RemainAfterExit = true; + + script = + (optionalString (!config.services.resolved.enable) '' + # Set the static DNS configuration, if given. + ${pkgs.openresolv}/sbin/resolvconf -m 1 -a static <, create a job ‘network-addresses-.service" + # that performs static address configuration. It has a "wants" + # dependency on ‘.service’, which is supposed to create + # the interface and need not exist (i.e. for hardware + # interfaces). It has a binds-to dependency on the actual + # network device, so it only gets started after the interface + # has appeared, and it's stopped when the interface + # disappears. + configureAddrs = i: + let + ips = interfaceIps i; + in + nameValuePair "network-addresses-${i.name}" + { description = "Addresss configuration of ${i.name}"; + wantedBy = [ "network-interfaces.target" ]; + before = [ "network-interfaces.target" ]; + bindsTo = [ (subsystemDevice i.name) ]; + after = [ (subsystemDevice i.name) ]; + serviceConfig.Type = "oneshot"; + serviceConfig.RemainAfterExit = true; + path = [ pkgs.iproute ]; + script = + '' + echo "bringing up interface..." + ip link set "${i.name}" up + + restart_network_interfaces=false + '' + flip concatMapStrings (ips) (ip: + let + address = "${ip.address}/${toString ip.prefixLength}"; + in + '' + echo "checking ip ${address}..." + if out=$(ip addr add "${address}" dev "${i.name}" 2>&1); then + echo "added ip ${address}..." + restart_network_setup=true + elif ! echo "$out" | grep "File exists" >/dev/null 2>&1; then + echo "failed to add ${address}" + exit 1 + fi + '') + + optionalString (ips != [ ]) + '' + if [ "$restart_network_setup" = "true" ]; then + # Ensure that the default gateway remains set. + # (Flushing this interface may have removed it.) + ${config.systemd.package}/bin/systemctl try-restart --no-block network-setup.service + fi + ${config.systemd.package}/bin/systemctl start ip-up.target + ''; + preStop = + '' + echo "releasing configured ip's..." + '' + flip concatMapStrings (ips) (ip: + let + address = "${ip.address}/${toString ip.prefixLength}"; + in + '' + echo -n "Deleting ${address}..." + ip addr del "${address}" dev "${i.name}" >/dev/null 2>&1 || echo -n " Failed" + echo "" + ''); + }; + + createTunDevice = i: nameValuePair "${i.name}-netdev" + { description = "Virtual Network Interface ${i.name}"; + requires = [ "dev-net-tun.device" ]; + after = [ "dev-net-tun.device" ]; + wantedBy = [ "network.target" (subsystemDevice i.name) ]; + path = [ pkgs.iproute ]; + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + }; + script = '' + ip tuntap add dev "${i.name}" \ + ${optionalString (i.virtualType != null) "mode ${i.virtualType}"} \ + user "${i.virtualOwner}" + ''; + postStop = '' + ip link del ${i.name} + ''; + }; + + createBridgeDevice = n: v: nameValuePair "${n}-netdev" + (let + deps = map subsystemDevice v.interfaces; + in + { description = "Bridge Interface ${n}"; + wantedBy = [ "network.target" (subsystemDevice n) ]; + bindsTo = deps; + after = deps; + serviceConfig.Type = "oneshot"; + serviceConfig.RemainAfterExit = true; + path = [ pkgs.iproute ]; + script = '' + # Remove Dead Interfaces + echo "Removing old bridge ${n}..." + ip link show "${n}" >/dev/null 2>&1 && ip link del "${n}" + + echo "Adding bridge ${n}..." + ip link add name "${n}" type bridge + + # Enslave child interfaces + ${flip concatMapStrings v.interfaces (i: '' + ip link set "${i}" master "${n}" + ip link set "${i}" up + '')} + + ip link set "${n}" up + ''; + postStop = '' + ip link set "${n}" down || true + ip link del "${n}" || true + ''; + }); + + createBondDevice = n: v: nameValuePair "${n}-netdev" + (let + deps = map subsystemDevice v.interfaces; + in + { description = "Bond Interface ${n}"; + wantedBy = [ "network.target" (subsystemDevice n) ]; + bindsTo = deps; + after = deps; + before = [ "${n}-cfg.service" ]; + serviceConfig.Type = "oneshot"; + serviceConfig.RemainAfterExit = true; + path = [ pkgs.iproute pkgs.gawk ]; + script = '' + echo "Destroying old bond ${n}..." + ${destroyBond n} + + echo "Creating new bond ${n}..." + ip link add name "${n}" type bond \ + ${optionalString (v.mode != null) "mode ${toString v.mode}"} \ + ${optionalString (v.miimon != null) "miimon ${toString v.miimon}"} \ + ${optionalString (v.xmit_hash_policy != null) "xmit_hash_policy ${toString v.xmit_hash_policy}"} \ + ${optionalString (v.lacp_rate != null) "lacp_rate ${toString v.lacp_rate}"} + + # !!! There must be a better way to wait for the interface + while [ ! -d "/sys/class/net/${n}" ]; do sleep 0.1; done; + + # Bring up the bond and enslave the specified interfaces + ip link set "${n}" up + ${flip concatMapStrings v.interfaces (i: '' + ip link set "${i}" master "${n}" + '')} + ''; + postStop = destroyBond n; + }); + + createMacvlanDevice = n: v: nameValuePair "${n}-netdev" + (let + deps = [ (subsystemDevice v.interface) ]; + in + { description = "Vlan Interface ${n}"; + wantedBy = [ "network.target" (subsystemDevice n) ]; + bindsTo = deps; + after = deps; + serviceConfig.Type = "oneshot"; + serviceConfig.RemainAfterExit = true; + path = [ pkgs.iproute ]; + script = '' + # Remove Dead Interfaces + ip link show "${n}" >/dev/null 2>&1 && ip link delete "${n}" + ip link add link "${v.interface}" name "${n}" type macvlan \ + ${optionalString (v.mode != null) "mode ${v.mode}"} + ip link set "${n}" up + ''; + postStop = '' + ip link delete "${n}" + ''; + }); + + createSitDevice = n: v: nameValuePair "${n}-netdev" + (let + deps = optional (v.dev != null) (subsystemDevice v.dev); + in + { description = "6-to-4 Tunnel Interface ${n}"; + wantedBy = [ "network.target" (subsystemDevice n) ]; + bindsTo = deps; + after = deps; + serviceConfig.Type = "oneshot"; + serviceConfig.RemainAfterExit = true; + path = [ pkgs.iproute ]; + script = '' + # Remove Dead Interfaces + ip link show "${n}" >/dev/null 2>&1 && ip link delete "${n}" + ip link add name "${n}" type sit \ + ${optionalString (v.remote != null) "remote \"${v.remote}\""} \ + ${optionalString (v.local != null) "local \"${v.local}\""} \ + ${optionalString (v.ttl != null) "ttl ${toString v.ttl}"} \ + ${optionalString (v.dev != null) "dev \"${v.dev}\""} + ip link set "${n}" up + ''; + postStop = '' + ip link delete "${n}" + ''; + }); + + createVlanDevice = n: v: nameValuePair "${n}-netdev" + (let + deps = [ (subsystemDevice v.interface) ]; + in + { description = "Vlan Interface ${n}"; + wantedBy = [ "network.target" (subsystemDevice n) ]; + bindsTo = deps; + after = deps; + serviceConfig.Type = "oneshot"; + serviceConfig.RemainAfterExit = true; + path = [ pkgs.iproute ]; + script = '' + # Remove Dead Interfaces + ip link show "${n}" >/dev/null 2>&1 && ip link delete "${n}" + ip link add link "${v.interface}" name "${n}" type vlan id "${toString v.id}" + ip link set "${n}" up + ''; + postStop = '' + ip link delete "${n}" + ''; + }); + + in listToAttrs ( + map configureAddrs interfaces ++ + map createTunDevice (filter (i: i.virtual) interfaces)) + // mapAttrs' createBridgeDevice cfg.bridges + // mapAttrs' createBondDevice cfg.bonds + // mapAttrs' createMacvlanDevice cfg.macvlans + // mapAttrs' createSitDevice cfg.sits + // mapAttrs' createVlanDevice cfg.vlans + // { + "network-setup" = networkSetup; + "network-local-commands" = networkLocalCommands; + }; + + services.udev.extraRules = + '' + KERNEL=="tun", TAG+="systemd" + ''; + + }; + +} diff --git a/nixos/modules/tasks/network-interfaces-systemd.nix b/nixos/modules/tasks/network-interfaces-systemd.nix new file mode 100644 index 000000000000..334b24b5ad38 --- /dev/null +++ b/nixos/modules/tasks/network-interfaces-systemd.nix @@ -0,0 +1,174 @@ +{ config, lib, pkgs, utils, ... }: + +with lib; +with utils; + +let + + cfg = config.networking; + interfaces = attrValues cfg.interfaces; + + interfaceIps = i: + i.ip4 ++ optionals cfg.enableIPv6 i.ip6 + ++ optional (i.ipAddress != null) { + address = i.ipAddress; + prefixLength = i.prefixLength; + } ++ optional (cfg.enableIPv6 && i.ipv6Address != null) { + address = i.ipv6Address; + prefixLength = i.ipv6PrefixLength; + }; + + dhcpStr = useDHCP: if useDHCP then "both" else "none"; + + slaves = + concatLists (map (bond: bond.interfaces) (attrValues cfg.bonds)) + ++ concatLists (map (bridge: bridge.interfaces) (attrValues cfg.bridges)) + ++ map (sit: sit.dev) (attrValues cfg.sits) + ++ map (vlan: vlan.interface) (attrValues cfg.vlans); + +in + +{ + + config = mkIf cfg.useNetworkd { + + assertions = [ { + assertion = cfg.defaultGatewayWindowSize == null; + message = "networking.defaultGatewayWindowSize is not supported by networkd."; + } ]; + + systemd.services.dhcpcd.enable = mkDefault false; + + systemd.services.network-local-commands = { + after = [ "systemd-networkd.service" ]; + bindsTo = [ "systemd-networkd.service" ]; + }; + + systemd.network = + let + domains = cfg.search ++ (optional (cfg.domain != null) cfg.domain); + genericNetwork = override: { + DHCP = override (dhcpStr cfg.useDHCP); + } // optionalAttrs (cfg.defaultGateway != null) { + gateway = override [ cfg.defaultGateway ]; + } // optionalAttrs (domains != [ ]) { + domains = override domains; + }; + in mkMerge [ { + enable = true; + networks."99-main" = genericNetwork mkDefault; + } + (mkMerge (flip map interfaces (i: { + netdevs = mkIf i.virtual ( + let + devType = if i.virtualType != null then i.virtualType + else (if hasPrefix "tun" i.name then "tun" else "tap"); + in { + "40-${i.name}" = { + netdevConfig = { + Name = i.name; + Kind = devType; + }; + "${devType}Config" = optionalAttrs (i.virtualOwner != null) { + User = i.virtualOwner; + }; + }; + }); + networks."40-${i.name}" = mkMerge [ (genericNetwork mkDefault) { + name = mkDefault i.name; + DHCP = mkForce (dhcpStr + (if i.useDHCP != null then i.useDHCP else interfaceIps i == [ ])); + address = flip map (interfaceIps i) + (ip: "${ip.address}/${toString ip.prefixLength}"); + } ]; + }))) + (mkMerge (flip mapAttrsToList cfg.bridges (name: bridge: { + netdevs."40-${name}" = { + netdevConfig = { + Name = name; + Kind = "bridge"; + }; + }; + networks = listToAttrs (flip map bridge.interfaces (bi: + nameValuePair "40-${bi}" (mkMerge [ (genericNetwork (mkOverride 999)) { + DHCP = mkOverride 0 (dhcpStr false); + networkConfig.Bridge = name; + } ]))); + }))) + (mkMerge (flip mapAttrsToList cfg.bonds (name: bond: { + netdevs."40-${name}" = { + netdevConfig = { + Name = name; + Kind = "bond"; + }; + bondConfig = + (optionalAttrs (bond.lacp_rate != null) { + LACPTransmitRate = bond.lacp_rate; + }) // (optionalAttrs (bond.miimon != null) { + MIIMonitorSec = bond.miimon; + }) // (optionalAttrs (bond.mode != null) { + Mode = bond.mode; + }) // (optionalAttrs (bond.xmit_hash_policy != null) { + TransmitHashPolicy = bond.xmit_hash_policy; + }); + }; + networks = listToAttrs (flip map bond.interfaces (bi: + nameValuePair "40-${bi}" (mkMerge [ (genericNetwork (mkOverride 999)) { + DHCP = mkOverride 0 (dhcpStr false); + networkConfig.Bond = name; + } ]))); + }))) + (mkMerge (flip mapAttrsToList cfg.macvlans (name: macvlan: { + netdevs."40-${name}" = { + netdevConfig = { + Name = name; + Kind = "macvlan"; + }; + macvlanConfig.Mode = macvlan.mode; + }; + networks."40-${macvlan.interface}" = (mkMerge [ (genericNetwork (mkOverride 999)) { + macvlan = [ name ]; + } ]); + }))) + (mkMerge (flip mapAttrsToList cfg.sits (name: sit: { + netdevs."40-${name}" = { + netdevConfig = { + Name = name; + Kind = "sit"; + }; + tunnelConfig = + (optionalAttrs (sit.remote != null) { + Remote = sit.remote; + }) // (optionalAttrs (sit.local != null) { + Local = sit.local; + }) // (optionalAttrs (sit.ttl != null) { + TTL = sit.ttl; + }); + }; + networks = mkIf (sit.dev != null) { + "40-${sit.dev}" = (mkMerge [ (genericNetwork (mkOverride 999)) { + tunnel = [ name ]; + } ]); + }; + }))) + (mkMerge (flip mapAttrsToList cfg.vlans (name: vlan: { + netdevs."40-${name}" = { + netdevConfig = { + Name = name; + Kind = "vlan"; + }; + vlanConfig.Id = vlan.id; + }; + networks."40-${vlan.interface}" = (mkMerge [ (genericNetwork (mkOverride 999)) { + vlan = [ name ]; + } ]); + }))) + ]; + + # We need to prefill the slaved devices with networking options + # This forces the network interface creator to initialize slaves. + networking.interfaces = listToAttrs (map (i: nameValuePair i { }) slaves); + + }; + +} diff --git a/nixos/modules/tasks/network-interfaces.nix b/nixos/modules/tasks/network-interfaces.nix index 9579eaa77d06..0ee2c9d2d00d 100644 --- a/nixos/modules/tasks/network-interfaces.nix +++ b/nixos/modules/tasks/network-interfaces.nix @@ -45,6 +45,16 @@ let description = "Name of the interface."; }; + useDHCP = mkOption { + type = types.nullOr types.bool; + default = null; + description = '' + Whether this interface should be configured with dhcp. + Null implies the old behavior which depends on whether ip addresses + are specified or not. + ''; + }; + ip4 = mkOption { default = [ ]; example = [ @@ -203,6 +213,7 @@ in networking.hostName = mkOption { default = "nixos"; + type = types.str; description = '' The name of the machine. Leave it empty if you want to obtain it from a DHCP server (if using DHCP). @@ -225,14 +236,16 @@ in networking.enableIPv6 = mkOption { default = true; + type = types.bool; description = '' Whether to enable support for IPv6. ''; }; networking.defaultGateway = mkOption { - default = ""; + default = null; example = "131.211.84.1"; + type = types.nullOr types.str; description = '' The default gateway. It can be left empty if it is auto-detected through DHCP. ''; @@ -266,8 +279,9 @@ in }; networking.domain = mkOption { - default = ""; + default = null; example = "home"; + type = types.nullOr types.str; description = '' The domain. It can be left empty if it is auto-detected through DHCP. ''; @@ -414,6 +428,37 @@ in }; }; + networking.macvlans = mkOption { + type = types.attrsOf types.optionSet; + default = { }; + example = { + wan = { + interface = "enp2s0"; + mode = "vepa"; + }; + }; + description = '' + This option allows you to define macvlan interfaces which should + be automatically created. + ''; + options = { + + interface = mkOption { + example = "enp4s0"; + type = types.string; + description = "The interface the macvlan will transmit packets through."; + }; + + mode = mkOption { + default = null; + type = types.nullOr types.str; + example = "vepa"; + description = "The mode of the macvlan device."; + }; + + }; + }; + networking.sits = mkOption { type = types.attrsOf types.optionSet; default = { }; @@ -523,6 +568,16 @@ in ''; }; + networking.useNetworkd = mkOption { + default = false; + type = types.bool; + description = '' + Whether we should use networkd as the network configuration backend or + the legacy script based system. Note that this option is experimental, + enable at your own risk. + ''; + }; + }; @@ -552,345 +607,18 @@ in # from being created. optionalString hasBonds "options bonding max_bonds=0"; - environment.systemPackages = - [ pkgs.host - pkgs.iproute - pkgs.iputils - pkgs.nettools - pkgs.wirelesstools - pkgs.iw - pkgs.rfkill - pkgs.openresolv - ] - ++ optional (cfg.bridges != {}) pkgs.bridge_utils - ++ optional hasVirtuals pkgs.tunctl - ++ optional cfg.enableIPv6 pkgs.ndisc6; + boot.kernel.sysctl = { + "net.net.ipv4.conf.all.promote_secondaries" = true; + "net.ipv6.conf.all.disable_ipv6" = mkDefault (!cfg.enableIPv6); + "net.ipv6.conf.default.disable_ipv6" = mkDefault (!cfg.enableIPv6); + "net.ipv4.conf.all_forwarding" = mkDefault (any (i: i.proxyARP) interfaces); + "net.ipv6.conf.all.forwarding" = mkDefault (any (i: i.proxyARP) interfaces); + } // listToAttrs (concatLists (flip map (filter (i: i.proxyARP) interfaces) + (i: flip map [ "4" "6" ] (v: nameValuePair "net.ipv${v}.conf.${i.name}.proxy_arp" true)) + )); security.setuidPrograms = [ "ping" "ping6" ]; - systemd.targets."network-interfaces" = - { description = "All Network Interfaces"; - wantedBy = [ "network.target" ]; - unitConfig.X-StopOnReconfiguration = true; - }; - - systemd.services = - let - - networkSetup = - { description = "Networking Setup"; - - after = [ "network-interfaces.target" ]; - before = [ "network.target" ]; - wantedBy = [ "network.target" ]; - - unitConfig.ConditionCapability = "CAP_NET_ADMIN"; - - path = [ pkgs.iproute ]; - - serviceConfig.Type = "oneshot"; - serviceConfig.RemainAfterExit = true; - - script = - '' - # Set the static DNS configuration, if given. - ${pkgs.openresolv}/sbin/resolvconf -m 1 -a static < /proc/sys/net/ipv6/conf/all/disable_ipv6 - fi - ''} - - # Set the default gateway. - ${optionalString (cfg.defaultGateway != "") '' - # FIXME: get rid of "|| true" (necessary to make it idempotent). - ip route add default via "${cfg.defaultGateway}" ${ - optionalString (cfg.defaultGatewayWindowSize != null) - "window ${cfg.defaultGatewayWindowSize}"} || true - ''} - - # Turn on forwarding if any interface has enabled proxy_arp. - ${optionalString (any (i: i.proxyARP) interfaces) '' - echo 1 > /proc/sys/net/ipv4/ip_forward - ''} - - # Run any user-specified commands. - ${cfg.localCommands} - ''; - }; - - # For each interface , create a job ‘-cfg.service" - # that performs static configuration. It has a "wants" - # dependency on ‘.service’, which is supposed to create - # the interface and need not exist (i.e. for hardware - # interfaces). It has a binds-to dependency on the actual - # network device, so it only gets started after the interface - # has appeared, and it's stopped when the interface - # disappears. - configureInterface = i: - let - ips = i.ip4 ++ optionals cfg.enableIPv6 i.ip6 - ++ optional (i.ipAddress != null) { - address = i.ipAddress; - prefixLength = i.prefixLength; - } ++ optional (cfg.enableIPv6 && i.ipv6Address != null) { - address = i.ipv6Address; - prefixLength = i.ipv6PrefixLength; - }; - in - nameValuePair "${i.name}-cfg" - { description = "Configuration of ${i.name}"; - wantedBy = [ "network-interfaces.target" ]; - bindsTo = [ (subsystemDevice i.name) ]; - after = [ (subsystemDevice i.name) ]; - serviceConfig.Type = "oneshot"; - serviceConfig.RemainAfterExit = true; - path = [ pkgs.iproute pkgs.gawk ]; - script = - '' - echo "bringing up interface..." - ip link set "${i.name}" up - '' - + optionalString (i.macAddress != null) - '' - echo "setting MAC address to ${i.macAddress}..." - ip link set "${i.name}" address "${i.macAddress}" - '' - + optionalString (i.mtu != null) - '' - echo "setting MTU to ${toString i.mtu}..." - ip link set "${i.name}" mtu "${toString i.mtu}" - '' - - # Ip Setup - + - '' - curIps=$(ip -o a show dev "${i.name}" | awk '{print $4}') - # Only do an add if it's necessary. This is - # useful when the Nix store is accessed via this - # interface (e.g. in a QEMU VM test). - '' - + flip concatMapStrings (ips) (ip: - let - address = "${ip.address}/${toString ip.prefixLength}"; - in - '' - echo "checking ip ${address}..." - if ! echo "$curIps" | grep "${address}" >/dev/null 2>&1; then - if out=$(ip addr add "${address}" dev "${i.name}" 2>&1); then - echo "added ip ${address}..." - restart_network_setup=true - elif ! echo "$out" | grep "File exists" >/dev/null 2>&1; then - echo "failed to add ${address}" - exit 1 - fi - fi - '') - + optionalString (ips != [ ]) - '' - if [ restart_network_setup = true ]; then - # Ensure that the default gateway remains set. - # (Flushing this interface may have removed it.) - ${config.systemd.package}/bin/systemctl try-restart --no-block network-setup.service - fi - ${config.systemd.package}/bin/systemctl start ip-up.target - '' - + optionalString i.proxyARP - '' - echo 1 > /proc/sys/net/ipv4/conf/${i.name}/proxy_arp - '' - + optionalString (i.proxyARP && cfg.enableIPv6) - '' - echo 1 > /proc/sys/net/ipv6/conf/${i.name}/proxy_ndp - ''; - preStop = - '' - echo "releasing configured ip's..." - '' - + flip concatMapStrings (ips) (ip: - let - address = "${ip.address}/${toString ip.prefixLength}"; - in - '' - echo -n "Deleting ${address}..." - ip addr del "${address}" dev "${i.name}" >/dev/null 2>&1 || echo -n " Failed" - echo "" - ''); - }; - - createTunDevice = i: nameValuePair "${i.name}-netdev" - { description = "Virtual Network Interface ${i.name}"; - requires = [ "dev-net-tun.device" ]; - after = [ "dev-net-tun.device" ]; - wantedBy = [ "network.target" (subsystemDevice i.name) ]; - path = [ pkgs.iproute ]; - serviceConfig = { - Type = "oneshot"; - RemainAfterExit = true; - }; - script = '' - ip tuntap add dev "${i.name}" \ - ${optionalString (i.virtualType != null) "mode ${i.virtualType}"} \ - user "${i.virtualOwner}" - ''; - postStop = '' - ip link del ${i.name} - ''; - }; - - createBridgeDevice = n: v: nameValuePair "${n}-netdev" - (let - deps = map subsystemDevice v.interfaces; - in - { description = "Bridge Interface ${n}"; - wantedBy = [ "network.target" (subsystemDevice n) ]; - bindsTo = deps; - after = deps; - serviceConfig.Type = "oneshot"; - serviceConfig.RemainAfterExit = true; - path = [ pkgs.bridge_utils pkgs.iproute ]; - script = - '' - # Remove Dead Interfaces - ip link show "${n}" >/dev/null 2>&1 && ip link delete "${n}" - - brctl addbr "${n}" - - # Set bridge's hello time to 0 to avoid startup delays. - brctl setfd "${n}" 0 - - ${flip concatMapStrings v.interfaces (i: '' - brctl addif "${n}" "${i}" - ip link set "${i}" up - ip addr flush dev "${i}" - - echo "bringing up network device ${n}..." - ip link set "${n}" up - '')} - - # !!! Should delete (brctl delif) any interfaces that - # no longer belong to the bridge. - ''; - postStop = - '' - ip link set "${n}" down - brctl delbr "${n}" - ''; - }); - - createBondDevice = n: v: nameValuePair "${n}-netdev" - (let - deps = map subsystemDevice v.interfaces; - in - { description = "Bond Interface ${n}"; - wantedBy = [ "network.target" (subsystemDevice n) ]; - bindsTo = deps; - after = deps; - before = [ "${n}-cfg.service" ]; - serviceConfig.Type = "oneshot"; - serviceConfig.RemainAfterExit = true; - path = [ pkgs.ifenslave pkgs.iproute ]; - script = '' - ip link add name "${n}" type bond - - # !!! There must be a better way to wait for the interface - while [ ! -d /sys/class/net/${n} ]; do sleep 0.1; done; - - # Ensure the link is down so that we can set options - ip link set "${n}" down - - # Set the miimon and mode options - ${optionalString (v.miimon != null) - "echo \"${toString v.miimon}\" >/sys/class/net/${n}/bonding/miimon"} - ${optionalString (v.mode != null) - "echo \"${v.mode}\" >/sys/class/net/${n}/bonding/mode"} - ${optionalString (v.lacp_rate != null) - "echo \"${v.lacp_rate}\" >/sys/class/net/${n}/bonding/lacp_rate"} - ${optionalString (v.xmit_hash_policy != null) - "echo \"${v.xmit_hash_policy}\" >/sys/class/net/${n}/bonding/xmit_hash_policy"} - - # Bring up the bond and enslave the specified interfaces - ip link set "${n}" up - ${flip concatMapStrings v.interfaces (i: '' - ifenslave "${n}" "${i}" - '')} - ''; - postStop = '' - ${flip concatMapStrings v.interfaces (i: '' - ifenslave -d "${n}" "${i}" >/dev/null 2>&1 || true - '')} - ip link set "${n}" down >/dev/null 2>&1 || true - ip link del "${n}" >/dev/null 2>&1 || true - ''; - }); - - createSitDevice = n: v: nameValuePair "${n}-netdev" - (let - deps = optional (v.dev != null) (subsystemDevice v.dev); - in - { description = "6-to-4 Tunnel Interface ${n}"; - wantedBy = [ "network.target" (subsystemDevice n) ]; - bindsTo = deps; - after = deps; - serviceConfig.Type = "oneshot"; - serviceConfig.RemainAfterExit = true; - path = [ pkgs.iproute ]; - script = '' - # Remove Dead Interfaces - ip link show "${n}" >/dev/null 2>&1 && ip link delete "${n}" - ip link add name "${n}" type sit \ - ${optionalString (v.remote != null) "remote \"${v.remote}\""} \ - ${optionalString (v.local != null) "local \"${v.local}\""} \ - ${optionalString (v.ttl != null) "ttl ${toString v.ttl}"} \ - ${optionalString (v.dev != null) "dev \"${v.dev}\""} - ip link set "${n}" up - ''; - postStop = '' - ip link delete "${n}" - ''; - }); - - createVlanDevice = n: v: nameValuePair "${n}-netdev" - (let - deps = [ (subsystemDevice v.interface) ]; - in - { description = "Vlan Interface ${n}"; - wantedBy = [ "network.target" (subsystemDevice n) ]; - bindsTo = deps; - after = deps; - serviceConfig.Type = "oneshot"; - serviceConfig.RemainAfterExit = true; - path = [ pkgs.iproute ]; - script = '' - # Remove Dead Interfaces - ip link show "${n}" >/dev/null 2>&1 && ip link delete "${n}" - ip link add link "${v.interface}" name "${n}" type vlan id "${toString v.id}" - ip link set "${n}" up - ''; - postStop = '' - ip link delete "${n}" - ''; - }); - - in listToAttrs ( - map configureInterface interfaces ++ - map createTunDevice (filter (i: i.virtual) interfaces)) - // mapAttrs' createBridgeDevice cfg.bridges - // mapAttrs' createBondDevice cfg.bonds - // mapAttrs' createSitDevice cfg.sits - // mapAttrs' createVlanDevice cfg.vlans - // { "network-setup" = networkSetup; }; - # Set the host and domain names in the activation script. Don't # clear it if it's not configured in the NixOS configuration, # since it may have been set by dhcpcd in the meantime. @@ -899,7 +627,7 @@ in hostname "${cfg.hostName}" ''; system.activationScripts.domain = - optionalString (cfg.domain != "") '' + optionalString (cfg.domain != null) '' domainname "${cfg.domain}" ''; @@ -918,11 +646,54 @@ in } ]; - services.udev.extraRules = - '' - KERNEL=="tun", TAG+="systemd" - ''; + environment.systemPackages = + [ pkgs.host + pkgs.iproute + pkgs.iputils + pkgs.nettools + pkgs.wirelesstools + pkgs.iw + pkgs.rfkill + pkgs.openresolv + ]; + systemd.services = { + network-local-commands = { + description = "Extra networking commands."; + before = [ "network.target" ]; + wantedBy = [ "network.target" ]; + unitConfig.ConditionCapability = "CAP_NET_ADMIN"; + path = [ pkgs.iproute ]; + serviceConfig.Type = "oneshot"; + serviceConfig.RemainAfterExit = true; + script = '' + # Run any user-specified commands. + ${cfg.localCommands} + ''; + }; + } // (listToAttrs (flip map interfaces (i: + nameValuePair "network-link-${i.name}" + { description = "Link configuration of ${i.name}"; + wantedBy = [ "network-interfaces.target" ]; + before = [ "network-interfaces.target" ]; + bindsTo = [ (subsystemDevice i.name) ]; + after = [ (subsystemDevice i.name) ]; + path = [ pkgs.iproute ]; + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + }; + script = + '' + echo "Configuring link..." + '' + optionalString (i.macAddress != null) '' + echo "setting MAC address to ${i.macAddress}..." + ip link set "${i.name}" address "${i.macAddress}" + '' + optionalString (i.mtu != null) '' + echo "setting MTU to ${toString i.mtu}..." + ip link set "${i.name}" mtu "${toString i.mtu}" + ''; + }))); }; } diff --git a/nixos/release-combined.nix b/nixos/release-combined.nix index 5173c33cab71..e850c1f6cddb 100644 --- a/nixos/release-combined.nix +++ b/nixos/release-combined.nix @@ -66,6 +66,14 @@ in rec { (all nixos.tests.misc) (all nixos.tests.nat.firewall) (all nixos.tests.nat.standalone) + (all nixos.tests.networking.scripted.static) + (all nixos.tests.networking.scripted.dhcpSimple) + (all nixos.tests.networking.scripted.dhcpOneIf) + (all nixos.tests.networking.scripted.bond) + (all nixos.tests.networking.scripted.bridge) + (all nixos.tests.networking.scripted.macvlan) + (all nixos.tests.networking.scripted.sit) + (all nixos.tests.networking.scripted.vlan) (all nixos.tests.nfs3) (all nixos.tests.openssh) (all nixos.tests.printing) diff --git a/nixos/release.nix b/nixos/release.nix index 7e2bfdb850b4..f3fe109b58ee 100644 --- a/nixos/release.nix +++ b/nixos/release.nix @@ -269,6 +269,22 @@ in rec { tests.mysqlReplication = callTest tests/mysql-replication.nix {}; tests.nat.firewall = callTest tests/nat.nix { withFirewall = true; }; tests.nat.standalone = callTest tests/nat.nix { withFirewall = false; }; + tests.networking.networkd.static = callTest tests/networking.nix { networkd = true; test = "static"; }; + tests.networking.networkd.dhcpSimple = callTest tests/networking.nix { networkd = true; test = "dhcpSimple"; }; + tests.networking.networkd.dhcpOneIf = callTest tests/networking.nix { networkd = true; test = "dhcpOneIf"; }; + tests.networking.networkd.bond = callTest tests/networking.nix { networkd = true; test = "bond"; }; + tests.networking.networkd.bridge = callTest tests/networking.nix { networkd = true; test = "bridge"; }; + tests.networking.networkd.macvlan = callTest tests/networking.nix { networkd = true; test = "macvlan"; }; + tests.networking.networkd.sit = callTest tests/networking.nix { networkd = true; test = "sit"; }; + tests.networking.networkd.vlan = callTest tests/networking.nix { networkd = true; test = "vlan"; }; + tests.networking.scripted.static = callTest tests/networking.nix { networkd = false; test = "static"; }; + tests.networking.scripted.dhcpSimple = callTest tests/networking.nix { networkd = false; test = "dhcpSimple"; }; + tests.networking.scripted.dhcpOneIf = callTest tests/networking.nix { networkd = false; test = "dhcpOneIf"; }; + tests.networking.scripted.bond = callTest tests/networking.nix { networkd = false; test = "bond"; }; + tests.networking.scripted.bridge = callTest tests/networking.nix { networkd = false; test = "bridge"; }; + tests.networking.scripted.macvlan = callTest tests/networking.nix { networkd = false; test = "macvlan"; }; + tests.networking.scripted.sit = callTest tests/networking.nix { networkd = false; test = "sit"; }; + tests.networking.scripted.vlan = callTest tests/networking.nix { networkd = false; test = "vlan"; }; tests.nfs3 = callTest tests/nfs.nix { version = 3; }; tests.nsd = callTest tests/nsd.nix {}; tests.openssh = callTest tests/openssh.nix {}; diff --git a/nixos/tests/networking.nix b/nixos/tests/networking.nix new file mode 100644 index 000000000000..b5e09cc3950b --- /dev/null +++ b/nixos/tests/networking.nix @@ -0,0 +1,365 @@ +import ./make-test.nix ({ networkd, test, ... }: + let + router = { config, pkgs, ... }: + with pkgs.lib; + let + vlanIfs = range 1 (length config.virtualisation.vlans); + in { + virtualisation.vlans = [ 1 2 3 ]; + networking = { + useDHCP = false; + useNetworkd = networkd; + firewall.allowPing = true; + interfaces = mkOverride 0 (listToAttrs (flip map vlanIfs (n: + nameValuePair "eth${toString n}" { + ipAddress = "192.168.${toString n}.1"; + prefixLength = 24; + }))); + }; + services.dhcpd = { + enable = true; + interfaces = map (n: "eth${toString n}") vlanIfs; + extraConfig = '' + option subnet-mask 255.255.255.0; + '' + flip concatMapStrings vlanIfs (n: '' + subnet 192.168.${toString n}.0 netmask 255.255.255.0 { + option broadcast-address 192.168.${toString n}.255; + option routers 192.168.${toString n}.1; + range 192.168.${toString n}.2 192.168.${toString n}.254; + } + ''); + }; + }; + testCases = { + static = { + name = "Static"; + nodes.router = router; + nodes.client = { config, pkgs, ... }: with pkgs.lib; { + virtualisation.vlans = [ 1 2 ]; + networking = { + useNetworkd = networkd; + firewall.allowPing = true; + useDHCP = false; + defaultGateway = "192.168.1.1"; + interfaces.eth1.ip4 = mkOverride 0 [ + { address = "192.168.1.2"; prefixLength = 24; } + { address = "192.168.1.3"; prefixLength = 32; } + { address = "192.168.1.10"; prefixLength = 32; } + ]; + interfaces.eth2.ip4 = mkOverride 0 [ + { address = "192.168.2.2"; prefixLength = 24; } + ]; + }; + }; + testScript = { nodes, ... }: + '' + startAll; + + $client->waitForUnit("network.target"); + $router->waitForUnit("network.target"); + + # Make sure dhcpcd is not started + $client->fail("systemctl status dhcpcd.service"); + + # Test vlan 1 + $client->succeed("ping -c 1 192.168.1.1"); + $client->succeed("ping -c 1 192.168.1.2"); + $client->succeed("ping -c 1 192.168.1.3"); + $client->succeed("ping -c 1 192.168.1.10"); + + $router->succeed("ping -c 1 192.168.1.1"); + $router->succeed("ping -c 1 192.168.1.2"); + $router->succeed("ping -c 1 192.168.1.3"); + $router->succeed("ping -c 1 192.168.1.10"); + + # Test vlan 2 + $client->succeed("ping -c 1 192.168.2.1"); + $client->succeed("ping -c 1 192.168.2.2"); + + $router->succeed("ping -c 1 192.168.2.1"); + $router->succeed("ping -c 1 192.168.2.2"); + + # Test default gateway + $router->succeed("ping -c 1 192.168.3.1"); + $client->succeed("ping -c 1 192.168.3.1"); + ''; + }; + dhcpSimple = { + name = "SimpleDHCP"; + nodes.router = router; + nodes.client = { config, pkgs, ... }: with pkgs.lib; { + virtualisation.vlans = [ 1 2 ]; + networking = { + useNetworkd = networkd; + firewall.allowPing = true; + useDHCP = true; + interfaces.eth1.ip4 = mkOverride 0 [ ]; + interfaces.eth2.ip4 = mkOverride 0 [ ]; + }; + }; + testScript = { nodes, ... }: + '' + startAll; + + $client->waitForUnit("network.target"); + $router->waitForUnit("network.target"); + $client->waitForUnit("dhcpcd.service"); + + # Wait until we have an ip address on each interface + $client->succeed("while ! ip addr show dev eth1 | grep '192.168.1'; do true; done"); + $client->succeed("while ! ip addr show dev eth2 | grep '192.168.2'; do true; done"); + + # Test vlan 1 + $client->succeed("ping -c 1 192.168.1.1"); + $client->succeed("ping -c 1 192.168.1.2"); + + $router->succeed("ping -c 1 192.168.1.1"); + $router->succeed("ping -c 1 192.168.1.2"); + + # Test vlan 2 + $client->succeed("ping -c 1 192.168.2.1"); + $client->succeed("ping -c 1 192.168.2.2"); + + $router->succeed("ping -c 1 192.168.2.1"); + $router->succeed("ping -c 1 192.168.2.2"); + ''; + }; + dhcpOneIf = { + name = "OneInterfaceDHCP"; + nodes.router = router; + nodes.client = { config, pkgs, ... }: with pkgs.lib; { + virtualisation.vlans = [ 1 2 ]; + networking = { + useNetworkd = networkd; + firewall.allowPing = true; + useDHCP = false; + interfaces.eth1 = { + ip4 = mkOverride 0 [ ]; + useDHCP = true; + }; + interfaces.eth2.ip4 = mkOverride 0 [ ]; + }; + }; + testScript = { nodes, ... }: + '' + startAll; + + $client->waitForUnit("network.target"); + $router->waitForUnit("network.target"); + $client->waitForUnit("dhcpcd.service"); + + # Wait until we have an ip address on each interface + $client->succeed("while ! ip addr show dev eth1 | grep '192.168.1'; do true; done"); + + # Test vlan 1 + $client->succeed("ping -c 1 192.168.1.1"); + $client->succeed("ping -c 1 192.168.1.2"); + + $router->succeed("ping -c 1 192.168.1.1"); + $router->succeed("ping -c 1 192.168.1.2"); + + # Test vlan 2 + $client->succeed("ping -c 1 192.168.2.1"); + $client->fail("ping -c 1 192.168.2.2"); + + $router->succeed("ping -c 1 192.168.2.1"); + $router->fail("ping -c 1 192.168.2.2"); + ''; + }; + bond = let + node = address: { config, pkgs, ... }: with pkgs.lib; { + virtualisation.vlans = [ 1 2 ]; + networking = { + useNetworkd = networkd; + firewall.allowPing = true; + useDHCP = false; + bonds.bond = { + mode = "balance-rr"; + interfaces = [ "eth1" "eth2" ]; + }; + interfaces.bond.ip4 = mkOverride 0 + [ { inherit address; prefixLength = 30; } ]; + }; + }; + in { + name = "Bond"; + nodes.client1 = node "192.168.1.1"; + nodes.client2 = node "192.168.1.2"; + testScript = { nodes, ... }: + '' + startAll; + + $client1->waitForUnit("network.target"); + $client2->waitForUnit("network.target"); + + # Test bonding + $client1->succeed("ping -c 2 192.168.1.1"); + $client1->succeed("ping -c 2 192.168.1.2"); + + $client2->succeed("ping -c 2 192.168.1.1"); + $client2->succeed("ping -c 2 192.168.1.2"); + ''; + }; + bridge = let + node = { address, vlan }: { config, pkgs, ... }: with pkgs.lib; { + virtualisation.vlans = [ vlan ]; + networking = { + useNetworkd = networkd; + firewall.allowPing = true; + useDHCP = false; + interfaces.eth1.ip4 = mkOverride 0 + [ { 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 = { config, pkgs, ... }: with pkgs.lib; { + virtualisation.vlans = [ 1 2 ]; + networking = { + useNetworkd = networkd; + firewall.allowPing = true; + useDHCP = false; + bridges.bridge.interfaces = [ "eth1" "eth2" ]; + interfaces.eth1.ip4 = mkOverride 0 [ ]; + interfaces.eth2.ip4 = mkOverride 0 [ ]; + interfaces.bridge.ip4 = mkOverride 0 + [ { address = "192.168.1.1"; prefixLength = 24; } ]; + }; + }; + testScript = { nodes, ... }: + '' + startAll; + + $client1->waitForUnit("network.target"); + $client2->waitForUnit("network.target"); + $router->waitForUnit("network.target"); + + # Test bridging + $client1->succeed("ping -c 1 192.168.1.1"); + $client1->succeed("ping -c 1 192.168.1.2"); + $client1->succeed("ping -c 1 192.168.1.3"); + + $client2->succeed("ping -c 1 192.168.1.1"); + $client2->succeed("ping -c 1 192.168.1.2"); + $client2->succeed("ping -c 1 192.168.1.3"); + + $router->succeed("ping -c 1 192.168.1.1"); + $router->succeed("ping -c 1 192.168.1.2"); + $router->succeed("ping -c 1 192.168.1.3"); + ''; + }; + macvlan = { + name = "MACVLAN"; + nodes.router = router; + nodes.client = { config, pkgs, ... }: with pkgs.lib; { + virtualisation.vlans = [ 1 ]; + networking = { + useNetworkd = networkd; + firewall.allowPing = true; + useDHCP = true; + macvlans.macvlan.interface = "eth1"; + interfaces.eth1.ip4 = mkOverride 0 [ ]; + }; + }; + testScript = { nodes, ... }: + '' + startAll; + + $client->waitForUnit("network.target"); + $router->waitForUnit("network.target"); + $client->waitForUnit("dhcpcd.service"); + + # Wait until we have an ip address on each interface + $client->succeed("while ! ip addr show dev eth1 | grep '192.168.1'; do true; done"); + $client->succeed("while ! ip addr show dev macvlan | grep '192.168.1'; do true; done"); + + # Test macvlan + $client->succeed("ping -c 1 192.168.1.1"); + $client->succeed("ping -c 1 192.168.1.2"); + $client->succeed("ping -c 1 192.168.1.3"); + + $router->succeed("ping -c 1 192.168.1.1"); + $router->succeed("ping -c 1 192.168.1.2"); + $router->succeed("ping -c 1 192.168.1.3"); + ''; + }; + sit = let + node = { address4, remote, address6 }: { config, pkgs, ... }: with pkgs.lib; { + virtualisation.vlans = [ 1 ]; + networking = { + useNetworkd = networkd; + firewall.enable = false; + useDHCP = false; + sits.sit = { + inherit remote; + local = address4; + dev = "eth1"; + }; + interfaces.eth1.ip4 = mkOverride 0 + [ { address = address4; prefixLength = 24; } ]; + interfaces.sit.ip6 = mkOverride 0 + [ { address = address6; prefixLength = 64; } ]; + }; + }; + in { + name = "Sit"; + 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 = { nodes, ... }: + '' + startAll; + + $client1->waitForUnit("network.target"); + $client2->waitForUnit("network.target"); + + $client1->succeed("ip addr >&2"); + $client2->succeed("ip addr >&2"); + + # Test ipv6 + $client1->succeed("ping6 -c 1 fc00::1"); + $client1->succeed("ping6 -c 1 fc00::2"); + + $client2->succeed("ping6 -c 1 fc00::1"); + $client2->succeed("ping6 -c 1 fc00::2"); + ''; + }; + vlan = let + node = address: { config, pkgs, ... }: with pkgs.lib; { + #virtualisation.vlans = [ 1 ]; + networking = { + useNetworkd = networkd; + firewall.allowPing = true; + useDHCP = false; + vlans.vlan = { + id = 1; + interface = "eth0"; + }; + interfaces.eth0.ip4 = mkOverride 0 [ ]; + interfaces.eth1.ip4 = mkOverride 0 [ ]; + interfaces.vlan.ip4 = mkOverride 0 + [ { inherit address; prefixLength = 24; } ]; + }; + }; + in { + name = "vlan"; + nodes.client1 = node "192.168.1.1"; + nodes.client2 = node "192.168.1.2"; + testScript = { nodes, ... }: + '' + startAll; + + $client1->waitForUnit("network.target"); + $client2->waitForUnit("network.target"); + + # Test vlan is setup + $client1->succeed("ip addr show dev vlan >&2"); + $client2->succeed("ip addr show dev vlan >&2"); + ''; + }; + }; + case = testCases.${test}; + in case // { + name = "${case.name}-Networking-${if networkd then "Networkd" else "Scripted"}"; + })