From 3a2161ef205c432db0053e9a82893069b54e55ed Mon Sep 17 00:00:00 2001 From: Gregor Kleen Date: Sun, 17 Oct 2021 20:50:47 +0200 Subject: yggdrasil-wg: ... --- hosts/sif/default.nix | 58 +- modules/networkd.nix | 297 ----- modules/networkd/default.nix | 1686 +++++++++++++++++++++++++++++ modules/networkd/systemd-lib.nix | 237 ++++ modules/networkd/systemd-unit-options.nix | 536 +++++++++ modules/yggdrasil-wg/default.nix | 234 ++-- 6 files changed, 2656 insertions(+), 392 deletions(-) delete mode 100644 modules/networkd.nix create mode 100644 modules/networkd/default.nix create mode 100644 modules/networkd/systemd-lib.nix create mode 100644 modules/networkd/systemd-unit-options.nix diff --git a/hosts/sif/default.nix b/hosts/sif/default.nix index 1658865a..43bd4485 100644 --- a/hosts/sif/default.nix +++ b/hosts/sif/default.nix @@ -70,32 +70,32 @@ ''; }; - wlanInterfaces = { - wlan0 = { - device = "wlp82s0"; - }; - }; - - bonds = { - "lan" = { - interfaces = [ "wlan0" "enp0s31f6" "dock0" ]; - driverOptions = { - miimon = "1000"; - mode = "active-backup"; - primary_reselect = "always"; - }; - }; - }; + # wlanInterfaces = { + # wlan0 = { + # device = "wlp82s0"; + # }; + # }; + + # bonds = { + # "lan" = { + # interfaces = [ "wlan0" "enp0s31f6" "dock0" ]; + # driverOptions = { + # miimon = "1000"; + # mode = "active-backup"; + # primary_reselect = "always"; + # }; + # }; + # }; dhcpcd.enable = false; useDHCP = false; useNetworkd = true; - interfaces."tinc.yggdrasil" = { - virtual = true; - virtualType = config.services.tinc.networks.yggdrasil.interfaceType; - macAddress = "5c:93:21:c3:61:39"; - }; + # interfaces."tinc.yggdrasil" = { + # virtual = true; + # virtualType = config.services.tinc.networks.yggdrasil.interfaceType; + # macAddress = "5c:93:21:c3:61:39"; + # }; }; systemd.services."NetworkManager-wait-online".enable = false; @@ -122,7 +122,7 @@ services = { udev.packages = with pkgs; [ uhk-agent ]; - tinc.yggdrasil.enable = true; + # tinc.yggdrasil.enable = true; uucp = { enable = true; @@ -274,13 +274,13 @@ daemonNiceLevel = 10; daemonIONiceLevel = 3; - buildServers.vidhar = { - address = "vidhar.yggdrasil"; - systems = ["x86_64-linux" "i686-linux"]; - maxJobs = 12; - speedFactor = 4; - supportedFeatures = ["nixos-test" "benchmark" "big-parallel" "kvm"]; - }; + # buildServers.vidhar = { + # address = "vidhar.yggdrasil"; + # systems = ["x86_64-linux" "i686-linux"]; + # maxJobs = 12; + # speedFactor = 4; + # supportedFeatures = ["nixos-test" "benchmark" "big-parallel" "kvm"]; + # }; }; environment.etc."X11/xorg.conf.d/50-wacom.conf".source = lib.mkForce ./wacom.conf; diff --git a/modules/networkd.nix b/modules/networkd.nix deleted file mode 100644 index d0a48f98..00000000 --- a/modules/networkd.nix +++ /dev/null @@ -1,297 +0,0 @@ -{ config, lib, utils, pkgs, ... }: - -with utils; -with lib; - -let - - cfg = config.networking; - interfaces = attrValues cfg.interfaces; - - interfaceIps = i: - i.ipv4.addresses - ++ optionals cfg.enableIPv6 i.ipv6.addresses; - - dhcpStr = useDHCP: if useDHCP == true || useDHCP == null then "yes" else "no"; - - 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) - # add dependency to physical or independently created vswitch member interface - # TODO: warn the user that any address configured on those interfaces will be useless - ++ concatMap (i: attrNames (filterAttrs (_: config: config.type != "internal") i.interfaces)) (attrValues cfg.vswitches); - -in - -{ - disabledModules = [ "tasks/network-interfaces-systemd.nix" ]; - - config = mkIf cfg.useNetworkd { - - assertions = [ { - assertion = cfg.defaultGatewayWindowSize == null; - message = "networking.defaultGatewayWindowSize is not supported by networkd."; - } { - assertion = cfg.vswitches == {}; - message = "networking.vswitches are not supported by networkd."; - } { - assertion = cfg.defaultGateway == null || cfg.defaultGateway.interface == null; - message = "networking.defaultGateway.interface is not supported by networkd."; - } { - assertion = cfg.defaultGateway6 == null || cfg.defaultGateway6.interface == null; - message = "networking.defaultGateway6.interface is not supported by networkd."; - } { - assertion = cfg.useDHCP == false; - message = '' - networking.useDHCP is not supported by networkd. - Please use per interface configuration and set the global option to false. - ''; - } ] ++ flip mapAttrsToList cfg.bridges (n: { rstp, ... }: { - assertion = !rstp; - message = "networking.bridges.${n}.rstp is not supported by networkd."; - }); - - networking.dhcpcd.enable = mkDefault false; - - systemd.network = - let - domains = cfg.search ++ (optional (cfg.domain != null) cfg.domain); - genericNetwork = override: - let gateways = optional (cfg.defaultGateway != null && (cfg.defaultGateway.address or "") != "") cfg.defaultGateway.address - ++ optional (cfg.defaultGateway6 != null && (cfg.defaultGateway6.address or "") != "") cfg.defaultGateway6.address; - in optionalAttrs (gateways != [ ]) { - routes = override (map (gateway: { - routeConfig = { - Gateway = gateway; - GatewayOnLink = false; - }; - }) gateways); - } // optionalAttrs (domains != [ ]) { - domains = override domains; - }; - in mkMerge [ { - enable = true; - } - (mkMerge (forEach interfaces (i: { - netdevs = mkIf i.virtual ({ - "40-${i.name}" = { - netdevConfig = { - Name = i.name; - Kind = i.virtualType; - }; - "${i.virtualType}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 false)); - address = forEach (interfaceIps i) - (ip: "${ip.address}/${toString ip.prefixLength}"); - networkConfig.IPv6PrivacyExtensions = "kernel"; - linkConfig = optionalAttrs (i.macAddress != null) { - MACAddress = i.macAddress; - } // optionalAttrs (i.mtu != null) { - MTUBytes = toString i.mtu; - }; - }]; - }))) - (mkMerge (flip mapAttrsToList cfg.bridges (name: bridge: { - netdevs."40-${name}" = { - netdevConfig = { - Name = name; - Kind = "bridge"; - }; - }; - networks = listToAttrs (forEach 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 = let - # manual mapping as of 2017-02-03 - # man 5 systemd.netdev [BOND] - # to https://www.kernel.org/doc/Documentation/networking/bonding.txt - # driver options. - driverOptionMapping = let - trans = f: optName: { valTransform = f; optNames = [optName]; }; - simp = trans id; - ms = trans (v: v + "ms"); - in { - Mode = simp "mode"; - TransmitHashPolicy = simp "xmit_hash_policy"; - LACPTransmitRate = simp "lacp_rate"; - MIIMonitorSec = ms "miimon"; - UpDelaySec = ms "updelay"; - DownDelaySec = ms "downdelay"; - LearnPacketIntervalSec = simp "lp_interval"; - AdSelect = simp "ad_select"; - FailOverMACPolicy = simp "fail_over_mac"; - ARPValidate = simp "arp_validate"; - # apparently in ms for this value?! Upstream bug? - ARPIntervalSec = simp "arp_interval"; - ARPIPTargets = simp "arp_ip_target"; - ARPAllTargets = simp "arp_all_targets"; - PrimaryReselectPolicy = simp "primary_reselect"; - ResendIGMP = simp "resend_igmp"; - PacketsPerSlave = simp "packets_per_slave"; - GratuitousARP = { valTransform = id; - optNames = [ "num_grat_arp" "num_unsol_na" ]; }; - AllSlavesActive = simp "all_slaves_active"; - MinLinks = simp "min_links"; - }; - - do = bond.driverOptions; - assertNoUnknownOption = let - knownOptions = flatten (mapAttrsToList (_: kOpts: kOpts.optNames) - driverOptionMapping); - # options that apparently don’t exist in the networkd config - unknownOptions = [ "primary" ]; - assertTrace = bool: msg: if bool then true else builtins.trace msg false; - in assert all (driverOpt: assertTrace - (elem driverOpt (knownOptions ++ unknownOptions)) - "The bond.driverOption `${driverOpt}` cannot be mapped to the list of known networkd bond options. Please add it to the mapping above the assert or to `unknownOptions` should it not exist in networkd.") - (mapAttrsToList (k: _: k) do); ""; - # get those driverOptions that have been set - filterSystemdOptions = filterAttrs (sysDOpt: kOpts: - any (kOpt: do ? ${kOpt}) kOpts.optNames); - # build final set of systemd options to bond values - buildOptionSet = mapAttrs (_: kOpts: with kOpts; - # we simply take the first set kernel bond option - # (one option has multiple names, which is silly) - head (map (optN: valTransform (do.${optN})) - # only map those that exist - (filter (o: do ? ${o}) optNames))); - in seq assertNoUnknownOption - (buildOptionSet (filterSystemdOptions driverOptionMapping)); - - }; - - networks = listToAttrs (forEach 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 = optionalAttrs (macvlan.mode != null) { 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); - - systemd.services = let - # We must escape interfaces due to the systemd interpretation - subsystemDevice = interface: - "sys-subsystem-net-devices-${escapeSystemdPath interface}.device"; - # support for creating openvswitch switches - createVswitchDevice = n: v: nameValuePair "${n}-netdev" - (let - deps = map subsystemDevice (attrNames (filterAttrs (_: config: config.type != "internal") v.interfaces)); - ofRules = pkgs.writeText "vswitch-${n}-openFlowRules" v.openFlowRules; - in - { description = "Open vSwitch Interface ${n}"; - wantedBy = [ "network.target" (subsystemDevice n) ]; - # and create bridge before systemd-networkd starts because it might create internal interfaces - before = [ "systemd-networkd.service" ]; - # shutdown the bridge when network is shutdown - partOf = [ "network.target" ]; - # requires ovs-vswitchd to be alive at all times - bindsTo = [ "ovs-vswitchd.service" ]; - # start switch after physical interfaces and vswitch daemon - after = [ "network-pre.target" "ovs-vswitchd.service" ] ++ deps; - wants = deps; # if one or more interface fails, the switch should continue to run - serviceConfig.Type = "oneshot"; - serviceConfig.RemainAfterExit = true; - path = [ pkgs.iproute2 config.virtualisation.vswitch.package ]; - preStart = '' - echo "Resetting Open vSwitch ${n}..." - ovs-vsctl --if-exists del-br ${n} -- add-br ${n} \ - -- set bridge ${n} protocols=${concatStringsSep "," v.supportedOpenFlowVersions} - ''; - script = '' - echo "Configuring Open vSwitch ${n}..." - ovs-vsctl ${concatStrings (mapAttrsToList (name: config: " -- add-port ${n} ${name}" + optionalString (config.vlan != null) " tag=${toString config.vlan}") v.interfaces)} \ - ${concatStrings (mapAttrsToList (name: config: optionalString (config.type != null) " -- set interface ${name} type=${config.type}") v.interfaces)} \ - ${concatMapStrings (x: " -- set-controller ${n} " + x) v.controllers} \ - ${concatMapStrings (x: " -- " + x) (splitString "\n" v.extraOvsctlCmds)} - - - echo "Adding OpenFlow rules for Open vSwitch ${n}..." - ovs-ofctl --protocols=${v.openFlowVersion} add-flows ${n} ${ofRules} - ''; - postStop = '' - echo "Cleaning Open vSwitch ${n}" - echo "Shuting down internal ${n} interface" - ip link set ${n} down || true - echo "Deleting flows for ${n}" - ovs-ofctl --protocols=${v.openFlowVersion} del-flows ${n} || true - echo "Deleting Open vSwitch ${n}" - ovs-vsctl --if-exists del-br ${n} || true - ''; - }); - in mapAttrs' createVswitchDevice cfg.vswitches - // { - "network-local-commands" = { - after = [ "systemd-networkd.service" ]; - bindsTo = [ "systemd-networkd.service" ]; - }; - }; - }; - -} diff --git a/modules/networkd/default.nix b/modules/networkd/default.nix new file mode 100644 index 00000000..007f14c6 --- /dev/null +++ b/modules/networkd/default.nix @@ -0,0 +1,1686 @@ +{ config, lib, pkgs, ... }: + +with lib; +with import ./systemd-unit-options.nix { inherit config lib; }; +with import ./systemd-lib.nix { inherit config lib pkgs; }; + +let + + cfg = config.systemd.network; + + check = { + + link = { + + sectionLink = checkUnitConfig "Link" [ + (assertOnlyFields [ + "Description" + "Alias" + "MACAddressPolicy" + "MACAddress" + "NamePolicy" + "Name" + "AlternativeNamesPolicy" + "AlternativeName" + "MTUBytes" + "BitsPerSecond" + "Duplex" + "AutoNegotiation" + "WakeOnLan" + "Port" + "Advertise" + "ReceiveChecksumOffload" + "TransmitChecksumOffload" + "TCPSegmentationOffload" + "TCP6SegmentationOffload" + "GenericSegmentationOffload" + "GenericReceiveOffload" + "LargeReceiveOffload" + "RxChannels" + "TxChannels" + "OtherChannels" + "CombinedChannels" + "RxBufferSize" + "TxBufferSize" + ]) + (assertValueOneOf "MACAddressPolicy" ["persistent" "random" "none"]) + (assertMacAddress "MACAddress") + (assertByteFormat "MTUBytes") + (assertByteFormat "BitsPerSecond") + (assertValueOneOf "Duplex" ["half" "full"]) + (assertValueOneOf "AutoNegotiation" boolValues) + (assertValueOneOf "WakeOnLan" ["phy" "unicast" "multicast" "broadcast" "arp" "magic" "secureon" "off"]) + (assertValueOneOf "Port" ["tp" "aui" "bnc" "mii" "fibre"]) + (assertValueOneOf "ReceiveChecksumOffload" boolValues) + (assertValueOneOf "TransmitChecksumOffload" boolValues) + (assertValueOneOf "TCPSegmentationOffload" boolValues) + (assertValueOneOf "TCP6SegmentationOffload" boolValues) + (assertValueOneOf "GenericSegmentationOffload" boolValues) + (assertValueOneOf "GenericReceiveOffload" boolValues) + (assertValueOneOf "LargeReceiveOffload" boolValues) + (assertInt "RxChannels") + (assertRange "RxChannels" 1 4294967295) + (assertInt "TxChannels") + (assertRange "TxChannels" 1 4294967295) + (assertInt "OtherChannels") + (assertRange "OtherChannels" 1 4294967295) + (assertInt "CombinedChannels") + (assertRange "CombinedChannels" 1 4294967295) + (assertInt "RxBufferSize") + (assertInt "TxBufferSize") + ]; + }; + + netdev = let + + tunChecks = [ + (assertOnlyFields [ + "MultiQueue" + "PacketInfo" + "VNetHeader" + "User" + "Group" + ]) + (assertValueOneOf "MultiQueue" boolValues) + (assertValueOneOf "PacketInfo" boolValues) + (assertValueOneOf "VNetHeader" boolValues) + ]; + in { + + sectionNetdev = checkUnitConfig "Netdev" [ + (assertOnlyFields [ + "Description" + "Name" + "Kind" + "MTUBytes" + "MACAddress" + ]) + (assertHasField "Name") + (assertHasField "Kind") + (assertValueOneOf "Kind" [ + "bond" + "bridge" + "dummy" + "gre" + "gretap" + "erspan" + "ip6gre" + "ip6tnl" + "ip6gretap" + "ipip" + "ipvlan" + "macvlan" + "macvtap" + "sit" + "tap" + "tun" + "veth" + "vlan" + "vti" + "vti6" + "vxlan" + "geneve" + "l2tp" + "macsec" + "vrf" + "vcan" + "vxcan" + "wireguard" + "netdevsim" + "nlmon" + "fou" + "xfrm" + "ifb" + "bareudp" + "batadv" + ]) + (assertByteFormat "MTUBytes") + (assertMacAddress "MACAddress") + ]; + + sectionVLAN = checkUnitConfig "VLAN" [ + (assertOnlyFields [ + "Id" + "GVRP" + "MVRP" + "LooseBinding" + "ReorderHeader" + ]) + (assertInt "Id") + (assertRange "Id" 0 4094) + (assertValueOneOf "GVRP" boolValues) + (assertValueOneOf "MVRP" boolValues) + (assertValueOneOf "LooseBinding" boolValues) + (assertValueOneOf "ReorderHeader" boolValues) + ]; + + sectionMACVLAN = checkUnitConfig "MACVLAN" [ + (assertOnlyFields [ + "Mode" + ]) + (assertValueOneOf "Mode" ["private" "vepa" "bridge" "passthru"]) + ]; + + sectionVXLAN = checkUnitConfig "VXLAN" [ + (assertOnlyFields [ + "VNI" + "Remote" + "Local" + "Group" + "TOS" + "TTL" + "MacLearning" + "FDBAgeingSec" + "MaximumFDBEntries" + "ReduceARPProxy" + "L2MissNotification" + "L3MissNotification" + "RouteShortCircuit" + "UDPChecksum" + "UDP6ZeroChecksumTx" + "UDP6ZeroChecksumRx" + "RemoteChecksumTx" + "RemoteChecksumRx" + "GroupPolicyExtension" + "GenericProtocolExtension" + "DestinationPort" + "PortRange" + "FlowLabel" + "IPDoNotFragment" + ]) + (assertInt "VNI") + (assertRange "VNI" 1 16777215) + (assertValueOneOf "MacLearning" boolValues) + (assertInt "MaximumFDBEntries") + (assertValueOneOf "ReduceARPProxy" boolValues) + (assertValueOneOf "L2MissNotification" boolValues) + (assertValueOneOf "L3MissNotification" boolValues) + (assertValueOneOf "RouteShortCircuit" boolValues) + (assertValueOneOf "UDPChecksum" boolValues) + (assertValueOneOf "UDP6ZeroChecksumTx" boolValues) + (assertValueOneOf "UDP6ZeroChecksumRx" boolValues) + (assertValueOneOf "RemoteChecksumTx" boolValues) + (assertValueOneOf "RemoteChecksumRx" boolValues) + (assertValueOneOf "GroupPolicyExtension" boolValues) + (assertValueOneOf "GenericProtocolExtension" boolValues) + (assertInt "FlowLabel") + (assertRange "FlowLabel" 0 1048575) + (assertValueOneOf "IPDoNotFragment" (boolValues + ["inherit"])) + ]; + + sectionTunnel = checkUnitConfig "Tunnel" [ + (assertOnlyFields [ + "Local" + "Remote" + "TOS" + "TTL" + "DiscoverPathMTU" + "IPv6FlowLabel" + "CopyDSCP" + "EncapsulationLimit" + "Key" + "InputKey" + "OutputKey" + "Mode" + "Independent" + "AssignToLoopback" + "AllowLocalRemote" + "FooOverUDP" + "FOUDestinationPort" + "FOUSourcePort" + "Encapsulation" + "IPv6RapidDeploymentPrefix" + "ISATAP" + "SerializeTunneledPackets" + "ERSPANIndex" + ]) + (assertInt "TTL") + (assertRange "TTL" 0 255) + (assertValueOneOf "DiscoverPathMTU" boolValues) + (assertValueOneOf "CopyDSCP" boolValues) + (assertValueOneOf "Mode" ["ip6ip6" "ipip6" "any"]) + (assertValueOneOf "Independent" boolValues) + (assertValueOneOf "AssignToLoopback" boolValues) + (assertValueOneOf "AllowLocalRemote" boolValues) + (assertValueOneOf "FooOverUDP" boolValues) + (assertPort "FOUDestinationPort") + (assertPort "FOUSourcePort") + (assertValueOneOf "Encapsulation" ["FooOverUDP" "GenericUDPEncapsulation"]) + (assertValueOneOf "ISATAP" boolValues) + (assertValueOneOf "SerializeTunneledPackets" boolValues) + (assertInt "ERSPANIndex") + (assertRange "ERSPANIndex" 1 1048575) + ]; + + sectionPeer = checkUnitConfig "Peer" [ + (assertOnlyFields [ + "Name" + "MACAddress" + ]) + (assertMacAddress "MACAddress") + ]; + + sectionTun = checkUnitConfig "Tun" tunChecks; + + sectionTap = checkUnitConfig "Tap" tunChecks; + + # NOTE The PrivateKey directive is missing on purpose here, please + # do not add it to this list. The nix store is world-readable let's + # refrain ourselves from providing a footgun. + sectionWireGuard = checkUnitConfig "WireGuard" [ + (assertOnlyFields [ + "PrivateKeyFile" + "ListenPort" + "FirewallMark" + ]) + (assertInt "FirewallMark") + (assertRange "FirewallMark" 1 4294967295) + ]; + + # NOTE The PresharedKey directive is missing on purpose here, please + # do not add it to this list. The nix store is world-readable,let's + # refrain ourselves from providing a footgun. + sectionWireGuardPeer = checkUnitConfig "WireGuardPeer" [ + (assertOnlyFields [ + "PublicKey" + "PresharedKeyFile" + "AllowedIPs" + "Endpoint" + "PersistentKeepalive" + ]) + (assertInt "PersistentKeepalive") + (assertRange "PersistentKeepalive" 0 65535) + ]; + + sectionBond = checkUnitConfig "Bond" [ + (assertOnlyFields [ + "Mode" + "TransmitHashPolicy" + "LACPTransmitRate" + "MIIMonitorSec" + "UpDelaySec" + "DownDelaySec" + "LearnPacketIntervalSec" + "AdSelect" + "AdActorSystemPriority" + "AdUserPortKey" + "AdActorSystem" + "FailOverMACPolicy" + "ARPValidate" + "ARPIntervalSec" + "ARPIPTargets" + "ARPAllTargets" + "PrimaryReselectPolicy" + "ResendIGMP" + "PacketsPerSlave" + "GratuitousARP" + "AllSlavesActive" + "DynamicTransmitLoadBalancing" + "MinLinks" + ]) + (assertValueOneOf "Mode" [ + "balance-rr" + "active-backup" + "balance-xor" + "broadcast" + "802.3ad" + "balance-tlb" + "balance-alb" + ]) + (assertValueOneOf "TransmitHashPolicy" [ + "layer2" + "layer3+4" + "layer2+3" + "encap2+3" + "encap3+4" + ]) + (assertValueOneOf "LACPTransmitRate" ["slow" "fast"]) + (assertValueOneOf "AdSelect" ["stable" "bandwidth" "count"]) + (assertInt "AdActorSystemPriority") + (assertRange "AdActorSystemPriority" 1 65535) + (assertInt "AdUserPortKey") + (assertRange "AdUserPortKey" 0 1023) + (assertValueOneOf "FailOverMACPolicy" ["none" "active" "follow"]) + (assertValueOneOf "ARPValidate" ["none" "active" "backup" "all"]) + (assertValueOneOf "ARPAllTargets" ["any" "all"]) + (assertValueOneOf "PrimaryReselectPolicy" ["always" "better" "failure"]) + (assertInt "ResendIGMP") + (assertRange "ResendIGMP" 0 255) + (assertInt "PacketsPerSlave") + (assertRange "PacketsPerSlave" 0 65535) + (assertInt "GratuitousARP") + (assertRange "GratuitousARP" 0 255) + (assertValueOneOf "AllSlavesActive" boolValues) + (assertValueOneOf "DynamicTransmitLoadBalancing" boolValues) + (assertInt "MinLinks") + (assertMinimum "MinLinks" 0) + ]; + + sectionXfrm = checkUnitConfig "Xfrm" [ + (assertOnlyFields [ + "InterfaceId" + "Independent" + ]) + (assertInt "InterfaceId") + (assertRange "InterfaceId" 1 4294967295) + (assertValueOneOf "Independent" boolValues) + ]; + + sectionVRF = checkUnitConfig "VRF" [ + (assertOnlyFields [ + "Table" + ]) + (assertInt "Table") + (assertMinimum "Table" 0) + ]; + }; + + network = { + + sectionLink = checkUnitConfig "Link" [ + (assertOnlyFields [ + "MACAddress" + "MTUBytes" + "ARP" + "Multicast" + "AllMulticast" + "Unmanaged" + "RequiredForOnline" + "ActivationPolicy" + ]) + (assertMacAddress "MACAddress") + (assertByteFormat "MTUBytes") + (assertValueOneOf "ARP" boolValues) + (assertValueOneOf "Multicast" boolValues) + (assertValueOneOf "AllMulticast" boolValues) + (assertValueOneOf "Unmanaged" boolValues) + (assertValueOneOf "RequiredForOnline" (boolValues ++ [ + "missing" + "off" + "no-carrier" + "dormant" + "degraded-carrier" + "carrier" + "degraded" + "enslaved" + "routable" + ])) + (assertValueOneOf "ActivationPolicy" ([ + "up" + "always-up" + "manual" + "always-down" + "down" + "bound" + ])) + ]; + + sectionNetwork = checkUnitConfig "Network" [ + (assertOnlyFields [ + "Description" + "DHCP" + "DHCPServer" + "LinkLocalAddressing" + "IPv4LLRoute" + "DefaultRouteOnDevice" + "IPv6Token" + "LLMNR" + "MulticastDNS" + "DNSOverTLS" + "DNSSEC" + "DNSSECNegativeTrustAnchors" + "LLDP" + "EmitLLDP" + "BindCarrier" + "Address" + "Gateway" + "DNS" + "Domains" + "DNSDefaultRoute" + "NTP" + "IPForward" + "IPMasquerade" + "IPv6PrivacyExtensions" + "IPv6AcceptRA" + "IPv6DuplicateAddressDetection" + "IPv6HopLimit" + "IPv4ProxyARP" + "IPv6ProxyNDP" + "IPv6ProxyNDPAddress" + "IPv6SendRA" + "DHCPv6PrefixDelegation" + "IPv6MTUBytes" + "BatmanAdvanced" + "Bridge" + "Bond" + "VRF" + "VLAN" + "IPVLAN" + "MACVLAN" + "VXLAN" + "Tunnel" + "MACsec" + "ActiveSlave" + "PrimarySlave" + "ConfigureWithoutCarrier" + "IgnoreCarrierLoss" + "Xfrm" + "KeepConfiguration" + ]) + # Note: For DHCP the values both, none, v4, v6 are deprecated + (assertValueOneOf "DHCP" ["yes" "no" "ipv4" "ipv6"]) + (assertValueOneOf "DHCPServer" boolValues) + (assertValueOneOf "LinkLocalAddressing" ["yes" "no" "ipv4" "ipv6" "fallback" "ipv4-fallback"]) + (assertValueOneOf "IPv4LLRoute" boolValues) + (assertValueOneOf "DefaultRouteOnDevice" boolValues) + (assertValueOneOf "LLMNR" (boolValues ++ ["resolve"])) + (assertValueOneOf "MulticastDNS" (boolValues ++ ["resolve"])) + (assertValueOneOf "DNSOverTLS" (boolValues ++ ["opportunistic"])) + (assertValueOneOf "DNSSEC" (boolValues ++ ["allow-downgrade"])) + (assertValueOneOf "LLDP" (boolValues ++ ["routers-only"])) + (assertValueOneOf "EmitLLDP" (boolValues ++ ["nearest-bridge" "non-tpmr-bridge" "customer-bridge"])) + (assertValueOneOf "DNSDefaultRoute" boolValues) + (assertValueOneOf "IPForward" (boolValues ++ ["ipv4" "ipv6"])) + (assertValueOneOf "IPMasquerade" boolValues) + (assertValueOneOf "IPv6PrivacyExtensions" (boolValues ++ ["prefer-public" "kernel"])) + (assertValueOneOf "IPv6AcceptRA" boolValues) + (assertInt "IPv6DuplicateAddressDetection") + (assertMinimum "IPv6DuplicateAddressDetection" 0) + (assertInt "IPv6HopLimit") + (assertMinimum "IPv6HopLimit" 0) + (assertValueOneOf "IPv4ProxyARP" boolValues) + (assertValueOneOf "IPv6ProxyNDP" boolValues) + (assertValueOneOf "IPv6SendRA" boolValues) + (assertValueOneOf "DHCPv6PrefixDelegation" boolValues) + (assertByteFormat "IPv6MTUBytes") + (assertValueOneOf "ActiveSlave" boolValues) + (assertValueOneOf "PrimarySlave" boolValues) + (assertValueOneOf "ConfigureWithoutCarrier" boolValues) + (assertValueOneOf "IgnoreCarrierLoss" boolValues) + (assertValueOneOf "KeepConfiguration" (boolValues ++ ["static" "dhcp-on-stop" "dhcp"])) + ]; + + sectionAddress = checkUnitConfig "Address" [ + (assertOnlyFields [ + "Address" + "Peer" + "Broadcast" + "Label" + "PreferredLifetime" + "Scope" + "HomeAddress" + "DuplicateAddressDetection" + "ManageTemporaryAddress" + "AddPrefixRoute" + "AutoJoin" + ]) + (assertHasField "Address") + (assertValueOneOf "PreferredLifetime" ["forever" "infinity" "0" 0]) + (assertValueOneOf "HomeAddress" boolValues) + (assertValueOneOf "DuplicateAddressDetection" ["ipv4" "ipv6" "both" "none"]) + (assertValueOneOf "ManageTemporaryAddress" boolValues) + (assertValueOneOf "AddPrefixRoute" boolValues) + (assertValueOneOf "AutoJoin" boolValues) + ]; + + sectionRoutingPolicyRule = checkUnitConfig "RoutingPolicyRule" [ + (assertOnlyFields [ + "TypeOfService" + "From" + "To" + "FirewallMark" + "Table" + "Priority" + "IncomingInterface" + "OutgoingInterface" + "SourcePort" + "DestinationPort" + "IPProtocol" + "InvertRule" + "Family" + "User" + "SuppressPrefixLength" + ]) + (assertInt "TypeOfService") + (assertRange "TypeOfService" 0 255) + (assertInt "FirewallMark") + (assertRange "FirewallMark" 1 4294967295) + (assertInt "Priority") + (assertPort "SourcePort") + (assertPort "DestinationPort") + (assertValueOneOf "InvertRule" boolValues) + (assertValueOneOf "Family" ["ipv4" "ipv6" "both"]) + (assertInt "SuppressPrefixLength") + (assertRange "SuppressPrefixLength" 0 128) + ]; + + sectionRoute = checkUnitConfig "Route" [ + (assertOnlyFields [ + "Gateway" + "GatewayOnLink" + "Destination" + "Source" + "Metric" + "IPv6Preference" + "Scope" + "PreferredSource" + "Table" + "Protocol" + "Type" + "InitialCongestionWindow" + "InitialAdvertisedReceiveWindow" + "QuickAck" + "FastOpenNoCookie" + "TTLPropagate" + "MTUBytes" + "IPServiceType" + "MultiPathRoute" + ]) + (assertValueOneOf "GatewayOnLink" boolValues) + (assertInt "Metric") + (assertValueOneOf "IPv6Preference" ["low" "medium" "high"]) + (assertValueOneOf "Scope" ["global" "site" "link" "host" "nowhere"]) + (assertValueOneOf "Type" [ + "unicast" + "local" + "broadcast" + "anycast" + "multicast" + "blackhole" + "unreachable" + "prohibit" + "throw" + "nat" + "xresolve" + ]) + (assertValueOneOf "QuickAck" boolValues) + (assertValueOneOf "FastOpenNoCookie" boolValues) + (assertValueOneOf "TTLPropagate" boolValues) + (assertByteFormat "MTUBytes") + (assertValueOneOf "IPServiceType" ["CS6" "CS4"]) + ]; + + sectionDHCPv4 = checkUnitConfig "DHCPv4" [ + (assertOnlyFields [ + "UseDNS" + "RoutesToDNS" + "UseNTP" + "UseSIP" + "UseMTU" + "Anonymize" + "SendHostname" + "UseHostname" + "Hostname" + "UseDomains" + "UseRoutes" + "UseTimezone" + "ClientIdentifier" + "VendorClassIdentifier" + "UserClass" + "MaxAttempts" + "DUIDType" + "DUIDRawData" + "IAID" + "RequestBroadcast" + "RouteMetric" + "RouteTable" + "RouteMTUBytes" + "ListenPort" + "SendRelease" + "SendDecline" + "BlackList" + "RequestOptions" + "SendOption" + ]) + (assertValueOneOf "UseDNS" boolValues) + (assertValueOneOf "RoutesToDNS" boolValues) + (assertValueOneOf "UseNTP" boolValues) + (assertValueOneOf "UseSIP" boolValues) + (assertValueOneOf "UseMTU" boolValues) + (assertValueOneOf "Anonymize" boolValues) + (assertValueOneOf "SendHostname" boolValues) + (assertValueOneOf "UseHostname" boolValues) + (assertValueOneOf "UseDomains" (boolValues ++ ["route"])) + (assertValueOneOf "UseRoutes" boolValues) + (assertValueOneOf "UseTimezone" boolValues) + (assertValueOneOf "ClientIdentifier" ["mac" "duid" "duid-only"]) + (assertInt "IAID") + (assertValueOneOf "RequestBroadcast" boolValues) + (assertInt "RouteMetric") + (assertInt "RouteTable") + (assertRange "RouteTable" 0 4294967295) + (assertByteFormat "RouteMTUBytes") + (assertPort "ListenPort") + (assertValueOneOf "SendRelease" boolValues) + (assertValueOneOf "SendDecline" boolValues) + ]; + + sectionDHCPv6 = checkUnitConfig "DHCPv6" [ + (assertOnlyFields [ + "UseAddress" + "UseDNS" + "UseNTP" + "RouteMetric" + "RapidCommit" + "MUDURL" + "RequestOptions" + "SendVendorOption" + "ForceDHCPv6PDOtherInformation" + "PrefixDelegationHint" + "WithoutRA" + "SendOption" + "UserClass" + "VendorClass" + ]) + (assertValueOneOf "UseAddress" boolValues) + (assertValueOneOf "UseDNS" boolValues) + (assertValueOneOf "UseNTP" boolValues) + (assertInt "RouteMetric") + (assertValueOneOf "RapidCommit" boolValues) + (assertValueOneOf "ForceDHCPv6PDOtherInformation" boolValues) + (assertValueOneOf "WithoutRA" ["solicit" "information-request"]) + (assertRange "SendOption" 1 65536) + ]; + + sectionDHCPv6PrefixDelegation = checkUnitConfig "DHCPv6PrefixDelegation" [ + (assertOnlyFields [ + "SubnetId" + "Announce" + "Assign" + "Token" + ]) + (assertValueOneOf "Announce" boolValues) + (assertValueOneOf "Assign" boolValues) + ]; + + sectionIPv6AcceptRA = checkUnitConfig "IPv6AcceptRA" [ + (assertOnlyFields [ + "UseDNS" + "UseDomains" + "RouteTable" + "UseAutonomousPrefix" + "UseOnLinkPrefix" + "RouterDenyList" + "RouterAllowList" + "PrefixDenyList" + "PrefixAllowList" + "RouteDenyList" + "RouteAllowList" + "DHCPv6Client" + ]) + (assertValueOneOf "UseDNS" boolValues) + (assertValueOneOf "UseDomains" (boolValues ++ ["route"])) + (assertRange "RouteTable" 0 4294967295) + (assertValueOneOf "UseAutonomousPrefix" boolValues) + (assertValueOneOf "UseOnLinkPrefix" boolValues) + (assertValueOneOf "DHCPv6Client" (boolValues ++ ["always"])) + ]; + + sectionDHCPServer = checkUnitConfig "DHCPServer" [ + (assertOnlyFields [ + "PoolOffset" + "PoolSize" + "DefaultLeaseTimeSec" + "MaxLeaseTimeSec" + "EmitDNS" + "DNS" + "EmitNTP" + "NTP" + "EmitSIP" + "SIP" + "EmitPOP3" + "POP3" + "EmitSMTP" + "SMTP" + "EmitLPR" + "LPR" + "EmitRouter" + "EmitTimezone" + "Timezone" + "SendOption" + "SendVendorOption" + ]) + (assertInt "PoolOffset") + (assertMinimum "PoolOffset" 0) + (assertInt "PoolSize") + (assertMinimum "PoolSize" 0) + (assertValueOneOf "EmitDNS" boolValues) + (assertValueOneOf "EmitNTP" boolValues) + (assertValueOneOf "EmitSIP" boolValues) + (assertValueOneOf "EmitPOP3" boolValues) + (assertValueOneOf "EmitSMTP" boolValues) + (assertValueOneOf "EmitLPR" boolValues) + (assertValueOneOf "EmitRouter" boolValues) + (assertValueOneOf "EmitTimezone" boolValues) + ]; + + sectionIPv6SendRA = checkUnitConfig "IPv6SendRA" [ + (assertOnlyFields [ + "Managed" + "OtherInformation" + "RouterLifetimeSec" + "RouterPreference" + "EmitDNS" + "DNS" + "EmitDomains" + "Domains" + "DNSLifetimeSec" + ]) + (assertValueOneOf "Managed" boolValues) + (assertValueOneOf "OtherInformation" boolValues) + (assertValueOneOf "RouterPreference" ["high" "medium" "low" "normal" "default"]) + (assertValueOneOf "EmitDNS" boolValues) + (assertValueOneOf "EmitDomains" boolValues) + ]; + + sectionIPv6Prefix = checkUnitConfig "IPv6Prefix" [ + (assertOnlyFields [ + "AddressAutoconfiguration" + "OnLink" + "Prefix" + "PreferredLifetimeSec" + "ValidLifetimeSec" + ]) + (assertValueOneOf "AddressAutoconfiguration" boolValues) + (assertValueOneOf "OnLink" boolValues) + ]; + + }; + }; + + commonNetworkOptions = { + + enable = mkOption { + default = true; + type = types.bool; + description = '' + Whether to manage network configuration using systemd-network. + ''; + }; + + 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. + ''; + }; + + extraConfig = mkOption { + default = ""; + type = types.lines; + description = "Extra configuration append to unit"; + }; + }; + + linkOptions = commonNetworkOptions // { + # overwrite enable option from above + enable = mkOption { + default = true; + type = types.bool; + description = '' + Whether to enable this .link unit. It's handled by udev no matter if systemd-networkd is enabled or not + ''; + }; + + linkConfig = mkOption { + default = {}; + example = { MACAddress = "00:ff:ee:aa:cc:dd"; }; + type = types.addCheck (types.attrsOf unitOption) check.link.sectionLink; + description = '' + Each attribute in this set specifies an option in the + [Link] section of the unit. See + systemd.link + 5 for details. + ''; + }; + + }; + + wireguardPeerOptions = { + options = { + wireguardPeerConfig = mkOption { + default = {}; + example = { }; + type = types.addCheck (types.attrsOf unitOption) check.netdev.sectionWireGuardPeer; + description = '' + Each attribute in this set specifies an option in the + [WireGuardPeer] section of the unit. See + systemd.network + 5 for details. + ''; + }; + }; + }; + + netdevOptions = commonNetworkOptions // { + + netdevConfig = mkOption { + default = {}; + example = { Name = "mybridge"; Kind = "bridge"; }; + type = types.addCheck (types.attrsOf unitOption) check.netdev.sectionNetdev; + 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) check.netdev.sectionVLAN; + 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) check.netdev.sectionMACVLAN; + 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) check.netdev.sectionVXLAN; + 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) check.netdev.sectionTunnel; + 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) check.netdev.sectionPeer; + 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) check.netdev.sectionTun; + 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) check.netdev.sectionTap; + description = '' + Each attribute in this set specifies an option in the + [Tap] section of the unit. See + systemd.netdev + 5 for details. + ''; + }; + + wireguardConfig = mkOption { + default = {}; + example = { + PrivateKeyFile = "/etc/wireguard/secret.key"; + ListenPort = 51820; + FwMark = 42; + }; + type = types.addCheck (types.attrsOf unitOption) check.netdev.sectionWireGuard; + description = '' + Each attribute in this set specifies an option in the + [WireGuard] section of the unit. See + systemd.netdev + 5 for details. + Use PrivateKeyFile instead of + PrivateKey: the nix store is + world-readable. + ''; + }; + + wireguardPeers = mkOption { + default = []; + example = [ { wireguardPeerConfig={ + Endpoint = "192.168.1.1:51820"; + PublicKey = "27s0OvaBBdHoJYkH9osZpjpgSOVNw+RaKfboT/Sfq0g="; + PresharedKeyFile = "/etc/wireguard/psk.key"; + AllowedIPs = [ "10.0.0.1/32" ]; + PersistentKeepalive = 15; + };}]; + type = with types; listOf (submodule wireguardPeerOptions); + description = '' + Each item in this array specifies an option in the + [WireGuardPeer] section of the unit. See + systemd.netdev + 5 for details. + Use PresharedKeyFile instead of + PresharedKey: the nix store is + world-readable. + ''; + }; + + bondConfig = mkOption { + default = {}; + example = { Mode = "802.3ad"; }; + type = types.addCheck (types.attrsOf unitOption) check.netdev.sectionBond; + description = '' + Each attribute in this set specifies an option in the + [Bond] section of the unit. See + systemd.netdev + 5 for details. + ''; + }; + + xfrmConfig = mkOption { + default = {}; + example = { InterfaceId = 1; }; + type = types.addCheck (types.attrsOf unitOption) check.netdev.sectionXfrm; + description = '' + Each attribute in this set specifies an option in the + [Xfrm] section of the unit. See + systemd.netdev + 5 for details. + ''; + }; + + vrfConfig = mkOption { + default = {}; + example = { Table = 2342; }; + type = types.addCheck (types.attrsOf unitOption) check.netdev.sectionVRF; + description = '' + Each attribute in this set specifies an option in the + [VRF] section of the unit. See + systemd.netdev + 5 for details. + A detailed explanation about how VRFs work can be found in the + kernel + docs. + ''; + }; + + }; + + addressOptions = { + options = { + addressConfig = mkOption { + default = {}; + example = { Address = "192.168.0.100/24"; }; + type = types.addCheck (types.attrsOf unitOption) check.network.sectionAddress; + description = '' + Each attribute in this set specifies an option in the + [Address] section of the unit. See + systemd.network + 5 for details. + ''; + }; + }; + }; + + routingPolicyRulesOptions = { + options = { + routingPolicyRuleConfig = mkOption { + default = { }; + example = { routingPolicyRuleConfig = { Table = 10; IncomingInterface = "eth1"; Family = "both"; } ;}; + type = types.addCheck (types.attrsOf unitOption) check.network.sectionRoutingPolicyRule; + description = '' + Each attribute in this set specifies an option in the + [RoutingPolicyRule] section of the unit. See + systemd.network + 5 for details. + ''; + }; + }; + }; + + routeOptions = { + options = { + routeConfig = mkOption { + default = {}; + example = { Gateway = "192.168.0.1"; }; + type = types.addCheck (types.attrsOf unitOption) check.network.sectionRoute; + description = '' + Each attribute in this set specifies an option in the + [Route] section of the unit. See + systemd.network + 5 for details. + ''; + }; + }; + }; + + ipv6PrefixOptions = { + options = { + ipv6PrefixConfig = mkOption { + default = {}; + example = { Prefix = "fd00::/64"; }; + type = types.addCheck (types.attrsOf unitOption) check.network.sectionIPv6Prefix; + description = '' + Each attribute in this set specifies an option in the + [IPv6Prefix] section of the unit. See + systemd.network + 5 for details. + ''; + }; + }; + }; + + networkOptions = commonNetworkOptions // { + + linkConfig = mkOption { + default = {}; + example = { Unmanaged = true; }; + type = types.addCheck (types.attrsOf unitOption) check.network.sectionLink; + description = '' + Each attribute in this set specifies an option in the + [Link] section of the unit. See + systemd.network + 5 for details. + ''; + }; + + networkConfig = mkOption { + default = {}; + example = { Description = "My Network"; }; + type = types.addCheck (types.attrsOf unitOption) check.network.sectionNetwork; + description = '' + Each attribute in this set specifies an option in the + [Network] section of the unit. See + systemd.network + 5 for details. + ''; + }; + + # systemd.network.networks.*.dhcpConfig has been deprecated in favor of ….dhcpV4Config + # Produce a nice warning message so users know it is gone. + dhcpConfig = mkOption { + visible = false; + apply = _: throw "The option `systemd.network.networks.*.dhcpConfig` can no longer be used since it's been removed. Please use `systemd.network.networks.*.dhcpV4Config` instead."; + }; + + dhcpV4Config = mkOption { + default = {}; + example = { UseDNS = true; UseRoutes = true; }; + type = types.addCheck (types.attrsOf unitOption) check.network.sectionDHCPv4; + description = '' + Each attribute in this set specifies an option in the + [DHCPv4] section of the unit. See + systemd.network + 5 for details. + ''; + }; + + dhcpV6Config = mkOption { + default = {}; + example = { UseDNS = true; UseRoutes = true; }; + type = types.addCheck (types.attrsOf unitOption) check.network.sectionDHCPv6; + description = '' + Each attribute in this set specifies an option in the + [DHCPv6] section of the unit. See + systemd.network + 5 for details. + ''; + }; + + dhcpV6PrefixDelegationConfig = mkOption { + default = {}; + example = { SubnetId = "auto"; Announce = true; }; + type = types.addCheck (types.attrsOf unitOption) check.network.sectionDHCPv6PrefixDelegation; + description = '' + Each attribute in this set specifies an option in the + [DHCPv6PrefixDelegation] section of the unit. See + systemd.network + 5 for details. + ''; + }; + + ipv6AcceptRAConfig = mkOption { + default = {}; + example = { UseDNS = true; DHCPv6Client = "always"; }; + type = types.addCheck (types.attrsOf unitOption) check.network.sectionIPv6AcceptRA; + description = '' + Each attribute in this set specifies an option in the + [IPv6AcceptRA] section of the unit. See + systemd.network + 5 for details. + ''; + }; + + dhcpServerConfig = mkOption { + default = {}; + example = { PoolOffset = 50; EmitDNS = false; }; + type = types.addCheck (types.attrsOf unitOption) check.network.sectionDHCPServer; + description = '' + Each attribute in this set specifies an option in the + [DHCPServer] section of the unit. See + systemd.network + 5 for details. + ''; + }; + + # systemd.network.networks.*.ipv6PrefixDelegationConfig has been deprecated + # in 247 in favor of systemd.network.networks.*.ipv6SendRAConfig. + ipv6PrefixDelegationConfig = mkOption { + visible = false; + apply = _: throw "The option `systemd.network.networks.*.ipv6PrefixDelegationConfig` has been replaced by `systemd.network.networks.*.ipv6SendRAConfig`."; + }; + + ipv6SendRAConfig = mkOption { + default = {}; + example = { EmitDNS = true; Managed = true; OtherInformation = true; }; + type = types.addCheck (types.attrsOf unitOption) check.network.sectionIPv6SendRA; + description = '' + Each attribute in this set specifies an option in the + [IPv6SendRA] section of the unit. See + systemd.network + 5 for details. + ''; + }; + + ipv6Prefixes = mkOption { + default = []; + example = { AddressAutoconfiguration = true; OnLink = true; }; + type = with types; listOf (submodule ipv6PrefixOptions); + description = '' + A list of ipv6Prefix sections to be added to 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. + ''; + }; + + bridge = mkOption { + default = [ ]; + type = types.listOf types.str; + description = '' + A list of bridge interfaces to be added to the network section of the + unit. See systemd.network + 5 for details. + ''; + }; + + bond = mkOption { + default = [ ]; + type = types.listOf types.str; + description = '' + A list of bond interfaces to be added to the network section of the + unit. See systemd.network + 5 for details. + ''; + }; + + vrf = mkOption { + default = [ ]; + type = types.listOf types.str; + description = '' + A list of vrf interfaces 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. + ''; + }; + + xfrm = mkOption { + default = [ ]; + type = types.listOf types.str; + description = '' + A list of xfrm interfaces to be added to the network section of the + unit. See systemd.network + 5 for details. + ''; + }; + + addresses = mkOption { + default = [ ]; + type = with types; listOf (submodule addressOptions); + description = '' + A list of address sections to be added to the unit. See + systemd.network + 5 for details. + ''; + }; + + routingPolicyRules = mkOption { + default = [ ]; + type = with types; listOf (submodule routingPolicyRulesOptions); + description = '' + A list of routing policy rules sections to be added to the unit. See + systemd.network + 5 for details. + ''; + }; + + routes = mkOption { + default = [ ]; + type = with types; listOf (submodule routeOptions); + description = '' + A list of route sections to be added to the unit. See + systemd.network + 5 for details. + ''; + }; + + }; + + networkConfig = { 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; + }; + }; + }; + + commonMatchText = def: optionalString (def.matchConfig != { }) '' + [Match] + ${attrsToSection def.matchConfig} + ''; + + linkToUnit = name: def: + { inherit (def) enable; + text = commonMatchText def + + '' + [Link] + ${attrsToSection def.linkConfig} + '' + + def.extraConfig; + }; + + 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.wireguardConfig != { }) '' + [WireGuard] + ${attrsToSection def.wireguardConfig} + '' + + flip concatMapStrings def.wireguardPeers (x: '' + [WireGuardPeer] + ${attrsToSection x.wireguardPeerConfig} + '') + + optionalString (def.bondConfig != { }) '' + [Bond] + ${attrsToSection def.bondConfig} + '' + + optionalString (def.xfrmConfig != { }) '' + [Xfrm] + ${attrsToSection def.xfrmConfig} + '' + + optionalString (def.vrfConfig != { }) '' + [VRF] + ${attrsToSection def.vrfConfig} + '' + + def.extraConfig; + }; + + networkToUnit = name: def: + { inherit (def) enable; + text = commonMatchText def + + optionalString (def.linkConfig != { }) '' + [Link] + ${attrsToSection def.linkConfig} + '' + + '' + [Network] + '' + + attrsToSection def.networkConfig + + optionalString (def.address != [ ]) '' + ${concatStringsSep "\n" (map (s: "Address=${s}") def.address)} + '' + + optionalString (def.gateway != [ ]) '' + ${concatStringsSep "\n" (map (s: "Gateway=${s}") def.gateway)} + '' + + optionalString (def.dns != [ ]) '' + ${concatStringsSep "\n" (map (s: "DNS=${s}") def.dns)} + '' + + optionalString (def.ntp != [ ]) '' + ${concatStringsSep "\n" (map (s: "NTP=${s}") def.ntp)} + '' + + optionalString (def.bridge != [ ]) '' + ${concatStringsSep "\n" (map (s: "Bridge=${s}") def.bridge)} + '' + + optionalString (def.bond != [ ]) '' + ${concatStringsSep "\n" (map (s: "Bond=${s}") def.bond)} + '' + + optionalString (def.vrf != [ ]) '' + ${concatStringsSep "\n" (map (s: "VRF=${s}") def.vrf)} + '' + + optionalString (def.vlan != [ ]) '' + ${concatStringsSep "\n" (map (s: "VLAN=${s}") def.vlan)} + '' + + optionalString (def.macvlan != [ ]) '' + ${concatStringsSep "\n" (map (s: "MACVLAN=${s}") def.macvlan)} + '' + + optionalString (def.vxlan != [ ]) '' + ${concatStringsSep "\n" (map (s: "VXLAN=${s}") def.vxlan)} + '' + + optionalString (def.tunnel != [ ]) '' + ${concatStringsSep "\n" (map (s: "Tunnel=${s}") def.tunnel)} + '' + + optionalString (def.xfrm != [ ]) '' + ${concatStringsSep "\n" (map (s: "Xfrm=${s}") def.xfrm)} + '' + + '' + + '' + + flip concatMapStrings def.addresses (x: '' + [Address] + ${attrsToSection x.addressConfig} + '') + + flip concatMapStrings def.routingPolicyRules (x: '' + [RoutingPolicyRule] + ${attrsToSection x.routingPolicyRuleConfig} + '') + + flip concatMapStrings def.routes (x: '' + [Route] + ${attrsToSection x.routeConfig} + '') + + optionalString (def.dhcpV4Config != { }) '' + [DHCPv4] + ${attrsToSection def.dhcpV4Config} + '' + + optionalString (def.dhcpV6Config != { }) '' + [DHCPv6] + ${attrsToSection def.dhcpV6Config} + '' + + optionalString (def.dhcpV6PrefixDelegationConfig != { }) '' + [DHCPv6PrefixDelegation] + ${attrsToSection def.dhcpV6PrefixDelegationConfig} + '' + + optionalString (def.ipv6AcceptRAConfig != { }) '' + [IPv6AcceptRA] + ${attrsToSection def.ipv6AcceptRAConfig} + '' + + optionalString (def.dhcpServerConfig != { }) '' + [DHCPServer] + ${attrsToSection def.dhcpServerConfig} + '' + + optionalString (def.ipv6SendRAConfig != { }) '' + [IPv6SendRA] + ${attrsToSection def.ipv6SendRAConfig} + '' + + flip concatMapStrings def.ipv6Prefixes (x: '' + [IPv6Prefix] + ${attrsToSection x.ipv6PrefixConfig} + '') + + def.extraConfig; + }; + + unitFiles = listToAttrs (map (name: { + name = "systemd/network/${name}"; + value.source = "${cfg.units.${name}.unit}/${name}"; + }) (attrNames cfg.units)); +in + +{ + disabledModules = [ "system/boot/networkd.nix" ]; + + options = { + + systemd.network.enable = mkOption { + default = false; + type = types.bool; + description = '' + Whether to enable networkd or not. + ''; + }; + + systemd.network.links = mkOption { + default = {}; + type = with types; attrsOf (submodule [ { options = linkOptions; } ]); + description = "Definition of systemd network links."; + }; + + systemd.network.netdevs = mkOption { + default = {}; + type = with types; attrsOf (submodule [ { options = netdevOptions; } ]); + description = "Definition of systemd network devices."; + }; + + systemd.network.networks = mkOption { + default = {}; + type = with types; attrsOf (submodule [ { options = networkOptions; } networkConfig ]); + description = "Definition of systemd networks."; + }; + + systemd.network.units = mkOption { + description = "Definition of networkd units."; + default = {}; + internal = true; + type = with types; attrsOf (submodule ( + { name, config, ... }: + { options = mapAttrs (_: x: x // { internal = true; }) concreteUnitOptions; + config = { + unit = mkDefault (makeUnit name config); + }; + })); + }; + + }; + + config = mkMerge [ + + # .link units are honored by udev, no matter if systemd-networkd is enabled or not. + { + systemd.network.units = mapAttrs' (n: v: nameValuePair "${n}.link" (linkToUnit n v)) cfg.links; + environment.etc = unitFiles; + } + + (mkIf config.systemd.network.enable { + + users.users.systemd-network.group = "systemd-network"; + + systemd.additionalUpstreamSystemUnits = [ + "systemd-networkd-wait-online.service" + "systemd-networkd.service" + "systemd-networkd.socket" + ]; + + systemd.network.units = mapAttrs' (n: v: nameValuePair "${n}.netdev" (netdevToUnit n v)) cfg.netdevs + // mapAttrs' (n: v: nameValuePair "${n}.network" (networkToUnit n v)) cfg.networks; + + # systemd-networkd is socket-activated by kernel netlink route change + # messages. It is important to have systemd buffer those on behalf of + # networkd. + systemd.sockets.systemd-networkd.wantedBy = [ "sockets.target" ]; + + systemd.services.systemd-networkd = { + wantedBy = [ "multi-user.target" ]; + aliases = [ "dbus-org.freedesktop.network1.service" ]; + restartTriggers = map (x: x.source) (attrValues unitFiles); + }; + + systemd.services.systemd-networkd-wait-online = { + wantedBy = [ "network-online.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; + }) + ]; +} diff --git a/modules/networkd/systemd-lib.nix b/modules/networkd/systemd-lib.nix new file mode 100644 index 00000000..2dbf1503 --- /dev/null +++ b/modules/networkd/systemd-lib.nix @@ -0,0 +1,237 @@ +{ config, lib, pkgs }: + +with lib; + +let + cfg = config.systemd; + lndir = "${pkgs.xorg.lndir}/bin/lndir"; +in rec { + + shellEscape = s: (replaceChars [ "\\" ] [ "\\\\" ] s); + + mkPathSafeName = lib.replaceChars ["@" ":" "\\" "[" "]"] ["-" "-" "-" "" ""]; + + makeUnit = name: unit: + if unit.enable then + pkgs.runCommand "unit-${mkPathSafeName name}" + { preferLocalBuild = true; + allowSubstitutes = false; + inherit (unit) text; + } + '' + mkdir -p $out + echo -n "$text" > $out/${shellEscape name} + '' + else + pkgs.runCommand "unit-${mkPathSafeName name}-disabled" + { preferLocalBuild = true; + allowSubstitutes = false; + } + '' + mkdir -p $out + ln -s /dev/null $out/${shellEscape name} + ''; + + boolValues = [true false "yes" "no"]; + + 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."; + + isPort = i: i >= 0 && i <= 65535; + + assertPort = name: group: attr: + optional (attr ? ${name} && ! isPort attr.${name}) + "Error on the systemd ${group} field `${name}': ${attr.name} is not a valid port number."; + + assertValueOneOf = name: values: group: attr: + optional (attr ? ${name} && !elem attr.${name} values) + "Systemd ${group} field `${name}' cannot have value `${toString attr.${name}}'."; + + assertHasField = name: group: attr: + optional (!(attr ? ${name})) + "Systemd ${group} field `${name}' must exist."; + + 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}]"; + + assertMinimum = name: min: group: attr: + optional (attr ? ${name} && attr.${name} < min) + "Systemd ${group} field `${name}' must be greater than or equal to ${toString min}"; + + assertOnlyFields = fields: group: attr: + let badFields = filter (name: ! elem name fields) (attrNames attr); in + optional (badFields != [ ]) + "Systemd ${group} has extra fields [${concatStringsSep " " badFields}]."; + + assertInt = name: group: attr: + optional (attr ? ${name} && !isInt attr.${name}) + "Systemd ${group} field `${name}' is not an integer"; + + checkUnitConfig = group: checks: attrs: let + # We're applied at the top-level type (attrsOf unitOption), so the actual + # unit options might contain attributes from mkOverride and mkIf that we need to + # convert into single values before checking them. + defs = mapAttrs (const (v: + if v._type or "" == "override" then v.content + else if v._type or "" == "if" then v.content + else v + )) attrs; + errors = concatMap (c: c group defs) checks; + in if errors == [] then true + else builtins.trace (concatStringsSep "\n" errors) false; + + toOption = x: + if x == true then "true" + else if x == false then "false" + else toString x; + + attrsToSection = as: + concatStrings (concatLists (mapAttrsToList (name: value: + map (x: '' + ${name}=${toOption x} + '') + (if isList value then value else [value])) + as)); + + generateUnits = generateUnits' true; + + generateUnits' = allowCollisions: type: units: upstreamUnits: upstreamWants: + pkgs.runCommand "${type}-units" + { preferLocalBuild = true; + allowSubstitutes = false; + } '' + mkdir -p $out + + # Copy the upstream systemd units we're interested in. + for i in ${toString upstreamUnits}; do + fn=${cfg.package}/example/systemd/${type}/$i + if ! [ -e $fn ]; then echo "missing $fn"; false; fi + if [ -L $fn ]; then + target="$(readlink "$fn")" + if [ ''${target:0:3} = ../ ]; then + ln -s "$(readlink -f "$fn")" $out/ + else + cp -pd $fn $out/ + fi + else + ln -s $fn $out/ + fi + done + + # Copy .wants links, but only those that point to units that + # we're interested in. + for i in ${toString upstreamWants}; do + fn=${cfg.package}/example/systemd/${type}/$i + if ! [ -e $fn ]; then echo "missing $fn"; false; fi + x=$out/$(basename $fn) + mkdir $x + for i in $fn/*; do + y=$x/$(basename $i) + cp -pd $i $y + if ! [ -e $y ]; then rm $y; fi + done + done + + # Symlink all units provided listed in systemd.packages. + packages="${toString cfg.packages}" + + # Filter duplicate directories + declare -A unique_packages + for k in $packages ; do unique_packages[$k]=1 ; done + + for i in ''${!unique_packages[@]}; do + for fn in $i/etc/systemd/${type}/* $i/lib/systemd/${type}/*; do + if ! [[ "$fn" =~ .wants$ ]]; then + if [[ -d "$fn" ]]; then + targetDir="$out/$(basename "$fn")" + mkdir -p "$targetDir" + ${lndir} "$fn" "$targetDir" + else + ln -s $fn $out/ + fi + fi + done + done + + # Symlink all units defined by systemd.units. If these are also + # provided by systemd or systemd.packages, then add them as + # .d/overrides.conf, which makes them extend the + # upstream unit. + for i in ${toString (mapAttrsToList (n: v: v.unit) units)}; do + fn=$(basename $i/*) + if [ -e $out/$fn ]; then + if [ "$(readlink -f $i/$fn)" = /dev/null ]; then + ln -sfn /dev/null $out/$fn + else + ${if allowCollisions then '' + mkdir -p $out/$fn.d + ln -s $i/$fn $out/$fn.d/overrides.conf + '' else '' + echo "Found multiple derivations configuring $fn!" + exit 1 + ''} + fi + else + ln -fs $i/$fn $out/ + fi + done + + # Create service aliases from aliases option. + ${concatStrings (mapAttrsToList (name: unit: + concatMapStrings (name2: '' + ln -sfn '${name}' $out/'${name2}' + '') unit.aliases) units)} + + # Create .wants and .requires symlinks from the wantedBy and + # requiredBy options. + ${concatStrings (mapAttrsToList (name: unit: + concatMapStrings (name2: '' + mkdir -p $out/'${name2}.wants' + ln -sfn '../${name}' $out/'${name2}.wants'/ + '') unit.wantedBy) units)} + + ${concatStrings (mapAttrsToList (name: unit: + concatMapStrings (name2: '' + mkdir -p $out/'${name2}.requires' + ln -sfn '../${name}' $out/'${name2}.requires'/ + '') unit.requiredBy) units)} + + ${optionalString (type == "system") '' + # Stupid misc. symlinks. + ln -s ${cfg.defaultUnit} $out/default.target + ln -s ${cfg.ctrlAltDelUnit} $out/ctrl-alt-del.target + ln -s rescue.target $out/kbrequest.target + + mkdir -p $out/getty.target.wants/ + ln -s ../autovt@tty1.service $out/getty.target.wants/ + + ln -s ../local-fs.target ../remote-fs.target \ + ../nss-lookup.target ../nss-user-lookup.target ../swap.target \ + $out/multi-user.target.wants/ + ''} + ''; # */ + +} diff --git a/modules/networkd/systemd-unit-options.nix b/modules/networkd/systemd-unit-options.nix new file mode 100644 index 00000000..4154389b --- /dev/null +++ b/modules/networkd/systemd-unit-options.nix @@ -0,0 +1,536 @@ +{ config, lib }: + +with lib; +with import ./systemd-lib.nix { inherit config lib pkgs; }; + +let + checkService = checkUnitConfig "Service" [ + (assertValueOneOf "Type" [ + "exec" "simple" "forking" "oneshot" "dbus" "notify" "idle" + ]) + (assertValueOneOf "Restart" [ + "no" "on-success" "on-failure" "on-abnormal" "on-abort" "always" + ]) + ]; + +in rec { + + unitOption = mkOptionType { + name = "systemd option"; + merge = loc: defs: + let + defs' = filterOverrides defs; + defs'' = getValues defs'; + in + if isList (head defs'') + then concatLists defs'' + else mergeEqualOption loc defs'; + }; + + sharedOptions = { + + enable = mkOption { + default = true; + type = types.bool; + description = '' + If set to false, this unit will be a symlink to + /dev/null. This is primarily useful to prevent specific + template instances + (e.g. serial-getty@ttyS0) from being + started. Note that enable=true does not + make a unit start by default at boot; if you want that, see + wantedBy. + ''; + }; + + requiredBy = mkOption { + default = []; + type = types.listOf types.str; + description = '' + Units that require (i.e. depend on and need to go down with) + this unit. The discussion under wantedBy + applies here as well: inverse .requires + symlinks are established. + ''; + }; + + wantedBy = mkOption { + default = []; + type = types.listOf types.str; + description = '' + Units that want (i.e. depend on) this unit. The standard way + to make a unit start by default at boot is to set this option + to [ "multi-user.target" ]. That's despite + the fact that the systemd.unit(5) manpage says this option + goes in the [Install] section that controls + the behaviour of systemctl enable. Since + such a process is stateful and thus contrary to the design of + NixOS, setting this option instead causes the equivalent + inverse .wants symlink to be present, + establishing the same desired relationship in a stateless way. + ''; + }; + + aliases = mkOption { + default = []; + type = types.listOf types.str; + description = "Aliases of that unit."; + }; + + }; + + concreteUnitOptions = sharedOptions // { + + text = mkOption { + type = types.nullOr types.str; + default = null; + description = "Text of this systemd unit."; + }; + + unit = mkOption { + internal = true; + description = "The generated unit."; + }; + + }; + + commonUnitOptions = sharedOptions // { + + description = mkOption { + default = ""; + type = types.str; + description = "Description of this unit used in systemd messages and progress indicators."; + }; + + documentation = mkOption { + default = []; + type = types.listOf types.str; + description = "A list of URIs referencing documentation for this unit or its configuration."; + }; + + requires = mkOption { + default = []; + type = types.listOf types.str; + description = '' + Start the specified units when this unit is started, and stop + this unit when the specified units are stopped or fail. + ''; + }; + + wants = mkOption { + default = []; + type = types.listOf types.str; + description = '' + Start the specified units when this unit is started. + ''; + }; + + after = mkOption { + default = []; + type = types.listOf types.str; + description = '' + If the specified units are started at the same time as + this unit, delay this unit until they have started. + ''; + }; + + before = mkOption { + default = []; + type = types.listOf types.str; + description = '' + If the specified units are started at the same time as + this unit, delay them until this unit has started. + ''; + }; + + bindsTo = mkOption { + default = []; + type = types.listOf types.str; + description = '' + Like ‘requires’, but in addition, if the specified units + unexpectedly disappear, this unit will be stopped as well. + ''; + }; + + partOf = mkOption { + default = []; + type = types.listOf types.str; + description = '' + If the specified units are stopped or restarted, then this + unit is stopped or restarted as well. + ''; + }; + + conflicts = mkOption { + default = []; + type = types.listOf types.str; + description = '' + If the specified units are started, then this unit is stopped + and vice versa. + ''; + }; + + 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"; }; + type = types.attrsOf unitOption; + description = '' + Each attribute in this set specifies an option in the + [Unit] section of the unit. See + systemd.unit + 5 for details. + ''; + }; + + restartTriggers = mkOption { + default = []; + type = types.listOf types.unspecified; + description = '' + An arbitrary list of items such as derivations. If any item + in the list changes between reconfigurations, the service will + be restarted. + ''; + }; + + onFailure = mkOption { + default = []; + type = types.listOf types.str; + description = '' + A list of one or more units that are activated when + this unit enters the "failed" state. + ''; + }; + + startLimitBurst = mkOption { + type = types.int; + description = '' + Configure unit start rate limiting. Units which are started + more than startLimitBurst times within an interval time + interval are not permitted to start any more. + ''; + }; + + startLimitIntervalSec = mkOption { + type = types.int; + description = '' + Configure unit start rate limiting. Units which are started + more than startLimitBurst times within an interval time + interval are not permitted to start any more. + ''; + }; + + }; + + + serviceOptions = commonUnitOptions // { + + environment = mkOption { + default = {}; + type = with types; attrsOf (nullOr (oneOf [ str path package ])); + example = { PATH = "/foo/bar/bin"; LANG = "nl_NL.UTF-8"; }; + description = "Environment variables passed to the service's processes."; + }; + + path = mkOption { + default = []; + type = with types; listOf (oneOf [ package str ]); + description = '' + Packages added to the service's PATH + environment variable. Both the bin + and sbin subdirectories of each + package are added. + ''; + }; + + serviceConfig = mkOption { + default = {}; + example = + { RestartSec = 5; + }; + type = types.addCheck (types.attrsOf unitOption) checkService; + description = '' + Each attribute in this set specifies an option in the + [Service] section of the unit. See + systemd.service + 5 for details. + ''; + }; + + script = mkOption { + type = types.lines; + default = ""; + description = "Shell commands executed as the service's main process."; + }; + + scriptArgs = mkOption { + type = types.str; + default = ""; + description = "Arguments passed to the main process script."; + }; + + preStart = mkOption { + type = types.lines; + default = ""; + description = '' + Shell commands executed before the service's main process + is started. + ''; + }; + + postStart = mkOption { + type = types.lines; + default = ""; + description = '' + Shell commands executed after the service's main process + is started. + ''; + }; + + reload = mkOption { + type = types.lines; + default = ""; + description = '' + Shell commands executed when the service's main process + is reloaded. + ''; + }; + + preStop = mkOption { + type = types.lines; + default = ""; + description = '' + Shell commands executed to stop the service. + ''; + }; + + postStop = mkOption { + type = types.lines; + default = ""; + description = '' + Shell commands executed after the service's main process + has exited. + ''; + }; + + restartIfChanged = mkOption { + type = types.bool; + default = true; + description = '' + Whether the service should be restarted during a NixOS + configuration switch if its definition has changed. + ''; + }; + + reloadIfChanged = mkOption { + type = types.bool; + default = false; + description = '' + Whether the service should be reloaded during a NixOS + configuration switch if its definition has changed. If + enabled, the value of is + ignored. + ''; + }; + + stopIfChanged = mkOption { + type = types.bool; + default = true; + description = '' + If set, a changed unit is restarted by calling + systemctl stop in the old configuration, + then systemctl start in the new one. + Otherwise, it is restarted in a single step using + systemctl restart in the new configuration. + The latter is less correct because it runs the + ExecStop commands from the new + configuration. + ''; + }; + + startAt = mkOption { + type = with types; either str (listOf str); + default = []; + example = "Sun 14:00:00"; + description = '' + Automatically start this unit at the given date/time, which + must be in the format described in + systemd.time + 7. This is equivalent + to adding a corresponding timer unit with + set to the value given here. + ''; + apply = v: if isList v then v else [ v ]; + }; + + }; + + + socketOptions = commonUnitOptions // { + + listenStreams = mkOption { + default = []; + type = types.listOf types.str; + example = [ "0.0.0.0:993" "/run/my-socket" ]; + description = '' + For each item in this list, a ListenStream + option in the [Socket] section will be created. + ''; + }; + + listenDatagrams = mkOption { + default = []; + type = types.listOf types.str; + example = [ "0.0.0.0:993" "/run/my-socket" ]; + description = '' + For each item in this list, a ListenDatagram + option in the [Socket] section will be created. + ''; + }; + + socketConfig = mkOption { + default = {}; + example = { ListenStream = "/run/my-socket"; }; + type = types.attrsOf unitOption; + description = '' + Each attribute in this set specifies an option in the + [Socket] section of the unit. See + systemd.socket + 5 for details. + ''; + }; + + }; + + + timerOptions = commonUnitOptions // { + + timerConfig = mkOption { + default = {}; + example = { OnCalendar = "Sun 14:00:00"; Unit = "foo.service"; }; + type = types.attrsOf unitOption; + description = '' + Each attribute in this set specifies an option in the + [Timer] section of the unit. See + systemd.timer + 5 and + systemd.time + 7 for details. + ''; + }; + + }; + + + pathOptions = commonUnitOptions // { + + pathConfig = mkOption { + default = {}; + example = { PathChanged = "/some/path"; Unit = "changedpath.service"; }; + type = types.attrsOf unitOption; + description = '' + Each attribute in this set specifies an option in the + [Path] section of the unit. See + systemd.path + 5 for details. + ''; + }; + + }; + + + mountOptions = commonUnitOptions // { + + what = mkOption { + example = "/dev/sda1"; + type = types.str; + description = "Absolute path of device node, file or other resource. (Mandatory)"; + }; + + where = mkOption { + example = "/mnt"; + type = types.str; + description = '' + Absolute path of a directory of the mount point. + Will be created if it doesn't exist. (Mandatory) + ''; + }; + + type = mkOption { + default = ""; + example = "ext4"; + type = types.str; + description = "File system type."; + }; + + options = mkOption { + default = ""; + example = "noatime"; + type = types.commas; + description = "Options used to mount the file system."; + }; + + mountConfig = mkOption { + default = {}; + example = { DirectoryMode = "0775"; }; + type = types.attrsOf unitOption; + description = '' + Each attribute in this set specifies an option in the + [Mount] section of the unit. See + systemd.mount + 5 for details. + ''; + }; + }; + + automountOptions = commonUnitOptions // { + + where = mkOption { + example = "/mnt"; + type = types.str; + description = '' + Absolute path of a directory of the mount point. + Will be created if it doesn't exist. (Mandatory) + ''; + }; + + automountConfig = mkOption { + default = {}; + example = { DirectoryMode = "0775"; }; + type = types.attrsOf unitOption; + description = '' + Each attribute in this set specifies an option in the + [Automount] section of the unit. See + systemd.automount + 5 for details. + ''; + }; + }; + + targetOptions = commonUnitOptions; + + sliceOptions = commonUnitOptions // { + + sliceConfig = mkOption { + default = {}; + example = { MemoryMax = "2G"; }; + type = types.attrsOf unitOption; + description = '' + Each attribute in this set specifies an option in the + [Slice] section of the unit. See + systemd.slice + 5 for details. + ''; + }; + + }; + +} diff --git a/modules/yggdrasil-wg/default.nix b/modules/yggdrasil-wg/default.nix index 8e2ba7a4..80443644 100644 --- a/modules/yggdrasil-wg/default.nix +++ b/modules/yggdrasil-wg/default.nix @@ -5,86 +5,183 @@ with lib; let listenPort = 51820; udp2rawPort = 51821; - subnet = "2a03:4000:52:ada:1"; - subnetLength = 80; - hostLength = subnetLength + 16; + wgSubnet = "2a03:4000:52:ada:1"; + wgSubnetLength = 80; + wgHostLength = wgSubnetLength + 16; + batSubnet = "2a03:4000:52:ada:2"; + batSubnetLength = 80; + batHostLength = batSubnetLength + 16; - links = [ + links = mkLinks [ { from = "vidhar"; to = "surtr"; endpointHost = "202.61.241.61"; - persistentKeepalive = 25; - dynamicEndpointRefreshSeconds = 86400; + udp2raw = true; + PersistentKeepalive = 25; } { from = "sif"; to = "surtr"; endpointHost = "202.61.241.61"; - persistentKeepalive = 25; - dynamicEndpointRefreshSeconds = 86400; + udp2raw = true; + PersistentKeepalive = 25; } - ]; - routes = [ { from = "sif"; to = "vidhar"; - via = "surtr"; - } - { from = "vidhar"; - to = "sif"; - via = "surtr"; + endpointHost = "192.168.2.168"; + PersistentKeepalive = 25; } ]; - hostIPs = { - surtr = ["${subnet}::/${toString hostLength}"]; - vidhar = ["${subnet}:1::/${toString hostLength}"]; - sif = ["${subnet}:2::/${toString hostLength}"]; + wgHostIPs = { + surtr = "${wgSubnet}::/${toString wgHostLength}"; + vidhar = "${wgSubnet}:1::/${toString wgHostLength}"; + sif = "${wgSubnet}:2::/${toString wgHostLength}"; + }; + greHostMACPrefixes = { + surtr = "02:00:00:00:00"; + vidhar = "02:00:00:00:01"; + sif = "02:00:00:00:02"; + }; + batHostIPs = { + surtr = ["${batSubnet}::/${toString batHostLength}"]; + vidhar = ["${batSubnet}:1::/${toString batHostLength}"]; + sif = ["${batSubnet}:2::/${toString batHostLength}"]; }; mkPublicKeyPath = host: ./hosts + "/${host}.pub"; mkPrivateKeyPath = host: ./hosts + "/${host}.priv"; + + kernel = config.boot.kernelPackages; publicKeyPath = mkPublicKeyPath hostName; privateKeyPath = mkPrivateKeyPath hostName; inNetwork = pathExists privateKeyPath && pathExists publicKeyPath; hostLinks = filter ({ from, to, ... }: from == hostName || to == hostName) links; - hostRoutes = filter ({ from, to, ... }: from == hostName || to == hostName) routes; - isRouter = inNetwork && any ({via, ...}: via == hostName) routes; - linkToPeer = ix: opts@{from, to, ...}: + # hostRoutes = filter ({ from, to, ... }: from == hostName || to == hostName) routes; + # isRouter = inNetwork && any ({via, ...}: via == hostName) routes; + linkToPeer = opts@{from, to, ...}: let other = if from == hostName then to else from; in { - allowedIPs = hostIPs.${other} ++ concatMap (rArgs: if rArgs.from != hostName || rArgs.via != to then [] else hostIPs.${rArgs.to}) routes; - publicKey = trim (readFile (mkPublicKeyPath other)); - } // (optionalAttrs (from == hostName) (filterAttrs (n: _v: !(elem n ["from" "to" "endpointHost"])) opts // optionalAttrs (opts ? "endpointHost") { endpoint = "127.0.0.1:${toString (udp2rawPort + ix)}"; })); - + AllowedIPs = wgHostIPs.${other}; # ++ concatMap (rArgs: if rArgs.from != hostName || rArgs.via != to then [] else wgHostIPs.${rArgs.to}) routes; + PublicKey = trim (readFile (mkPublicKeyPath other)); + } // (optionalAttrs (from == hostName) (filterAttrs (n: _v: !(elem n ["from" "to" "endpointHost" "udp2raw"])) opts // optionalAttrs (opts ? "endpointHost") (if opts ? "udp2raw" then { Endpoint = "127.0.0.1:${toString (udp2rawPort + opts.udp2raw)}"; } else { Endpoint = "${opts.endpointHost}:${toString listenPort}"; }))); + linkToGreDev = opts@{from, to, ...}: + let + other = if from == hostName then to else from; + in nameValuePair "yggdrasil-gre-${other}" { + netdevConfig = { + Name = "yggdrasil-gre-${other}"; + Kind = "ip6gretap"; + MTUBytes = toString 1280; + }; + tunnelConfig = { + Local = stripSubnet wgHostIPs.${hostName}; + Remote = stripSubnet wgHostIPs.${other}; + }; + }; + linkToGreNetwork = ix: opts@{from, to, ...}: + let + other = if from == hostName then to else from; + hexIx = let + hexIx' = toHexString ix; + in if (stringLength hexIx' < 2) then "0${hexIx'}" else hexIx'; + in nameValuePair "yggdrasil-gre-${other}" { + matchConfig = { + Name = "yggdrasil-gre-${other}"; + }; + linkConfig = { + MACAddress = "${greHostMACPrefixes.${hostName}}:${hexIx}"; + }; + networkConfig = { + Tunnel = "yggdrasil-gre-${other}"; + BatmanAdvanced = "yggdrasil"; + }; + linkConfig = { + RequiredForOnline = false; + }; + }; + trim = str: if hasSuffix "\n" str then trim (removeSuffix "\n" str) else str; stripSubnet = addr: let matchRes = builtins.match "^(.*)/[0-9]+$" addr; in if matchRes == null then addr else elemAt matchRes 0; + optIx = optName: xs: let + withOpts = listToAttrs (imap0 (ix: x: nameValuePair x.name (x.value // { ${optName} = ix; })) (filter (x: x.value.${optName} or false) (imap0 (ix: nameValuePair (toString ix)) xs))); + withoutOpts = listToAttrs (map (nv: nameValuePair nv.name (removeAttrs nv.value [optName])) (filter (x: !(x.value.${optName} or false)) (imap0 (ix: nameValuePair (toString ix)) xs))); + in genList (ix: withOpts.${toString ix} or withoutOpts.${toString ix}) (length xs); + mkLinks = optIx "udp2raw"; in { config = { assertions = [ { assertion = inNetwork || !(pathExists privateKeyPath || pathExists publicKeyPath); message = "yggdrasil-wg: Either both public and private keys must exist or neither."; } - { assertion = !inNetwork || (hostIPs ? "${hostName}"); - message = "yggdrasil-wg: Entry in hostIPs must exist."; + { assertion = !inNetwork || (wgHostIPs ? "${hostName}"); + message = "yggdrasil-wg: Entry in wgHostIPs must exist."; } ] ++ map ({from, to, ...}: let other = if from == hostName then to else from; in { assertion = pathExists (mkPublicKeyPath other); message = "yggdrasil-wg: This host (${hostName}) has a link with ‘${other}’, but no public key is available for ‘${other}’."; }) hostLinks; - networking.wireguard.interfaces = mkIf inNetwork { - yggdrasil = { - allowedIPsAsRoutes = false; - inherit listenPort; - ips = hostIPs.${hostName}; - peers = filter (value: value != null) (imap0 (ix: opts@{to, from, ...}: if from == hostName || to == hostName then linkToPeer ix opts else null) links); - privateKeyFile = config.sops.secrets."yggdrasil-wg.priv".path; - postSetup = '' - ip li set mtu 1280 dev yggdrasil - ${concatMapStringsSep "\n" (linkArgs: let other = if linkArgs.from == hostName then linkArgs.to else linkArgs.from; in concatMapStringsSep "\n" (otherIP: "ip route replace \"${otherIP}\" dev \"yggdrasil\" table \"main\"") hostIPs.${other}) hostLinks} - ${concatMapStringsSep "\n" (routeArgs: let other = if routeArgs.from == hostName then routeArgs.to else routeArgs.from; in concatMapStringsSep "\n" (otherIP: concatMapStringsSep "\n" (viaIP: "ip route replace \"${otherIP}\" via \"${viaIP}\" dev \"yggdrasil\" table \"main\"") (map stripSubnet hostIPs.${routeArgs.via})) hostIPs.${other}) hostRoutes} - ''; - }; + systemd.network = mkIf inNetwork { + enable = true; + netdevs = { + yggdrasil-wg = { + netdevConfig = { + Name = "yggdrasil-wg"; + Kind = "wireguard"; + MTUBytes = toString (1280 + 70); + }; + wireguardConfig = { + PrivateKeyFile = config.sops.secrets."yggdrasil-wg.priv".path; + ListenPort = listenPort; + }; + wireguardPeers = map (opts@{to, from, ...}: { wireguardPeerConfig = linkToPeer opts; }) hostLinks; + }; + yggdrasil = { + netdevConfig = { + Name = "yggdrasil"; + Kind = "batadv"; + }; + }; + } // listToAttrs (map linkToGreDev hostLinks); + + networks = { + yggdrasil-wg = { + name = "yggdrasil-wg"; + matchConfig = { + Name = "yggdrasil-wg"; + }; + address = [wgHostIPs.${hostName}]; + linkConfig = { + RequiredForOnline = false; + }; + }; + yggdrasil = { + name = "yggdrasil"; + matchConfig = { + Name = "yggdrasil"; + }; + address = batHostIPs.${hostName}; + linkConfig = { + RequiredForOnline = false; + }; + }; + } // listToAttrs (imap0 linkToGreNetwork hostLinks); }; - systemd.services = listToAttrs (filter ({ value, ...}: value != null) (imap0 (ix: opts@{to, from, ...}: let other = if from == hostName then to else from; in nameValuePair "yggdrasil-udp2raw@${other}" (if opts ? "endpointHost" && (from == hostName || to == hostName) then { + # networking.wireguard.interfaces = mkIf inNetwork { + # yggdrasil = { + # allowedIPsAsRoutes = false; + # inherit listenPort; + # ips = wgHostIPs.${hostName}; + # peers = filter (value: value != null) (map (opts@{to, from, ...}: if from == hostName || to == hostName then linkToPeer opts else null) links); + # privateKeyFile = config.sops.secrets."yggdrasil-wg.priv".path; + # postSetup = '' + # ip li set mtu 1280 dev yggdrasil + # ${concatMapStringsSep "\n" (linkArgs: let other = if linkArgs.from == hostName then linkArgs.to else linkArgs.from; in concatMapStringsSep "\n" (otherIP: "ip route replace \"${otherIP}\" dev \"yggdrasil\" table \"main\"") wgHostIPs.${other}) hostLinks} + # ${concatMapStringsSep "\n" (routeArgs: let other = if routeArgs.from == hostName then routeArgs.to else routeArgs.from; in concatMapStringsSep "\n" (otherIP: concatMapStringsSep "\n" (viaIP: "ip route replace \"${otherIP}\" via \"${viaIP}\" dev \"yggdrasil\" table \"main\"") (map stripSubnet wgHostIPs.${routeArgs.via})) wgHostIPs.${other}) hostRoutes} + # ''; + # }; + # }; + + systemd.services = listToAttrs (filter ({ value, ...}: value != null) (map (opts@{to, from, ...}: let other = if from == hostName then to else from; in nameValuePair "yggdrasil-udp2raw@${other}" (if opts ? "endpointHost" && opts ? "udp2raw" then { path = with pkgs; [iptables]; serviceConfig = { RuntimeDirectory = ["udp2raw-config-${other}"]; @@ -95,16 +192,17 @@ in { cat >''${RUNTIME_DIRECTORY}/udp2raw.conf <