From 75891c310390bd6e94511d1cf58b8bdafeb502d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20Schwarz=C3=A4ugl?= Date: Fri, 2 Jan 2026 05:03:32 +0100 Subject: [PATCH] feat[server]: finalize router config --- SwarselSystems.org | 635 ++++++++++++------ .../nixos/x86_64-linux/hintbooth/default.nix | 12 +- hosts/nixos/x86_64-linux/pyramid/default.nix | 10 +- .../x86_64-linux/pyramid/disk-config.nix | 2 +- .../nixos/optional/systemd-networkd-base.nix | 13 + .../optional/systemd-networkd-server-home.nix | 131 ++++ .../optional/systemd-networkd-server.nix | 28 +- .../nixos/optional/systemd-networkd-vlan.nix | 116 ---- modules/nixos/server/disk-encrypt.nix | 5 +- modules/nixos/server/kea.nix | 50 +- modules/nixos/server/network.nix | 9 +- modules/nixos/server/router.nix | 102 ++- modules/shared/options.nix | 12 + secrets/repo/globals.nix.enc | 6 +- 14 files changed, 739 insertions(+), 392 deletions(-) create mode 100644 modules/nixos/optional/systemd-networkd-base.nix create mode 100644 modules/nixos/optional/systemd-networkd-server-home.nix delete mode 100644 modules/nixos/optional/systemd-networkd-vlan.nix diff --git a/SwarselSystems.org b/SwarselSystems.org index b0d8309..b7c2b09 100644 --- a/SwarselSystems.org +++ b/SwarselSystems.org @@ -2994,7 +2994,7 @@ My work machine. Built for more security, this is the gold standard of my config main = { # name = "BOE 0x0BC9 Unknown"; name = "BOE 0x0BC9"; - mode = "2560x1600"; # TEMPLATE + mode = "2560x1600"; scale = "1"; position = "2560,0"; workspace = "15:L"; @@ -3008,10 +3008,10 @@ My work machine. Built for more security, this is the gold standard of my config personal = true; }; - networking.nftables = { - enable = lib.mkForce false; - firewall.enable = lib.mkForce false; - }; + # networking.nftables = { + # enable = lib.mkForce false; + # firewall.enable = lib.mkForce false; + # }; } #+end_src @@ -3179,7 +3179,7 @@ My work machine. Built for more security, this is the gold standard of my config fileSystems = { "/persist".neededForBoot = true; "/home".neededForBoot = true; - "/".neededForBoot = true; + "/".neededForBoot = true; # this is ok because this is not a impermanence host "/var/log".neededForBoot = true; }; } @@ -3891,15 +3891,14 @@ This is my main server that I run at home. It handles most tasks that require bi :CUSTOM_ID: h:624b3c6a-6e31-4734-a6ea-7c5b461a3429 :END: #+begin_src nix-ts :tangle hosts/nixos/x86_64-linux/hintbooth/default.nix - { self, config, lib, minimal, confLib, ... }: + { self, config, lib, minimal, confLib, globals, ... }: { imports = [ ./hardware-configuration.nix ./disk-config.nix - "${self}/modules/nixos/optional/systemd-networkd-server.nix" - "${self}/modules/nixos/optional/systemd-networkd-vlan.nix" + "${self}/modules/nixos/optional/systemd-networkd-server-home.nix" ]; topology.self = { @@ -3913,7 +3912,10 @@ This is my main server that I run at home. It handles most tasks that require bi }; }; - globals.general.homeProxy = config.node.name; + globals.general = { + homeProxy = config.node.name; + routerServer = config.node.name; + }; swarselsystems = { info = "HUNSN RM02, 8GB RAM"; @@ -3928,6 +3930,8 @@ This is my main server that I run at home. It handles most tasks that require bi swapSize = "8G"; networkKernelModules = [ "igb" ]; withMicroVMs = true; + localVLANs = map (name: "${name}") (builtins.attrNames globals.networks.home-lan.vlans); + initrdVLAN = "home"; server = { wireguard.interfaces = { wgHome = { @@ -9164,6 +9168,9 @@ Auto login for the initial session. #+end_src **** Firezone Client +:PROPERTIES: +:CUSTOM_ID: h:4d018a21-637b-4c7d-b9c9-7f1b95144a07 +:END: #+begin_src nix-ts :tangle modules/nixos/client/firezone-client.nix @@ -9756,19 +9763,20 @@ Restricts access to the system by the nix build user as per https://discourse.ni } #+end_src -**** Network settings +**** Network settings (globals.networks population) :PROPERTIES: :CUSTOM_ID: h:0ff3acc5-9ce8-4b22-a2e2-f6f1e69d47a5 :END: Generate hostId using =head -c4 /dev/urandom | od -A none -t x4= +This section is mainly used to populate entries in =globals.networks= with the interfaces defined in the local secrets of the respective host. Also, we expose some convenient values under =globals.hosts= and setup basic networking. + #+begin_src nix-ts :tangle modules/nixos/server/network.nix { lib, config, ... }: let netConfig = config.repo.secrets.local.networking; netPrefix = "${if config.swarselsystems.isCloud then config.node.name else "home"}"; - # netName = "${netPrefix}-${config.swarselsystems.server.localNetwork}"; in { options = { @@ -9794,11 +9802,6 @@ Generate hostId using =head -c4 /dev/urandom | od -A none -t x4= swarselsystems.server.localNetwork = netConfig.localNetwork or ""; - # globals.networks.${netName}.hosts.${config.node.name} = { - # inherit (netConfig.networks.${netConfig.localNetwork}) id; - # mac = netConfig.networks.${netConfig.localNetwork}.mac or null; - # }; - globals.networks = lib.mapAttrs' (netName: _: lib.nameValuePair "${netPrefix}-${netName}" { @@ -9811,7 +9814,8 @@ Generate hostId using =head -c4 /dev/urandom | od -A none -t x4= netConfig.networks; globals.hosts.${config.node.name} = { - inherit (config.repo.secrets.local.networking) defaultGateway4; + defaultGateway4 = netConfig.defaultGateway4 or null; + defaultGateway6 = netConfig.defaultGateway6 or null; wanAddress4 = netConfig.wanAddress4 or null; wanAddress6 = netConfig.wanAddress6 or null; isHome = if (netPrefix == "home") then true else false; @@ -9873,6 +9877,9 @@ I also take some precautions in how I get networking information during stage 1. subnetMask = globals.networks.${config.swarselsystems.server.netConfigName}.subnetMask4; gatewayIp = globals.hosts.${config.node.name}.defaultGateway4; + inherit (globals.general) routerServer; + isRouter = config.node.name == routerServer; + hostKeyPathBase = "/etc/secrets/initrd/ssh_host_ed25519_key"; hostKeyPath = if config.swarselsystems.isImpermanence then @@ -9911,7 +9918,7 @@ I also take some precautions in how I get networking information during stage 1. }; boot = lib.mkIf (!config.swarselsystems.isClient) { - kernelParams = lib.mkIf (!config.swarselsystems.isCloud) [ + kernelParams = lib.mkIf (!config.swarselsystems.isCloud && ((config.swarselsystems.localVLANs == []) || isRouter)) [ "ip=${localIp}::${gatewayIp}:${subnetMask}:${config.networking.hostName}::none" ]; initrd = { @@ -10342,12 +10349,43 @@ In order to define a new wireguard interface, I have to: :CUSTOM_ID: h:b54f2bbb-0088-46b2-957d-fd8234b772c3 :END: -This is the configuration to make [[#h:58c7563e-6954-42e6-a622-9d06523e8e24][Hintbooth (Router: HUNSN RM02)]] act as the router for my internal network. This is not a reusable module and highly adapted to its hardware. +This is the configuration to make [[#h:58c7563e-6954-42e6-a622-9d06523e8e24][Hintbooth (Router: HUNSN RM02)]] act as the router for my internal network. This is not a reusable module and highly adapted to its hardware. Below is a rough sketch of the functionality: + + - six LAN ports, five of which are bridged + - lan1–lan5 are enslaved into said bridge and behave as a single VLAN‑aware switch + - all VLANs are defined under =globals.networks.home-lan.vlans= + - for each VLAN, a routed interface me-${vlanName} is created on the router (NOTE: this interface also serves as the communication link to the local microvms - the respective extra interfaces are defined in [[#h:049fc27e-a28f-4ff0-b5f0-d81401bdd56f][systemd-networkd (server home)]]) + - RA and forwarding are enabled on these me-* interfaces so the router advertises vlanCfg.cidrv6 and routes between VLANs / WAN / WireGuard + - the sixth LAN port is used as the WAN / untrusted uplink to the =Fritz!Box= + - the mapping from MAC addresses to interfaces is defined in =config.repo.secrets.local.networking.networks..mac= (and performed in [[#h:99bf6c0e-2566-4a50-b219-fb6a7d4fb2cd][systemd-networkd (base)]] using =renameInterfacesByMac=) + - connectivity to microvms should not be lost in case there is no cable connected to the router + - this is achieved by connecting the veth interface pair veth-br / veth-int + - veth-br is part of the bridge and carries all VLANs tagged, as if it were another physical switch port + - veth-int stays on the host side and is used as the internal attachment point for microvms / guests + - ConfigureWithoutCarrier and ActivationPolicy = "always-up" are used so that the bridge and veth side stay UP; this however does not guarantee connectivity by itself as the kernel will not route packets if the underlying interface is not up (see also [[#h:049fc27e-a28f-4ff0-b5f0-d81401bdd56f][systemd-networkd (server home)]]) + - nftables firewall is derived from the same VLAN definitions: + - a zone =vlan-*= is created for each VLAN and bound to =me-*=, as well as zones for WAN, WG, and DNS + - all internal =vlan-*= zones are allowed to go to untrusted; NAT is implemented via a custom postrouting chain that masquerades both IPv4 and IPv6 traffic + - any VLAN with internet access is allowed to reach AdGuardHome for DNS (access-adguardhome-dns) + - this is important so that we can make use of the internal nginx instance to prevent bottlenecks over the web proxy + - policy between internal networks: + - the home VLAN is allowed to access the services and devices VLANs + - the services VLAN is allowed to reach selected ports on local (currently wireguard) + - WireGuard peers in wgHome are allowed to talk to each other (wgHome → wgHome) + - global IPv4/IPv6 forwarding is enabled via boot.kernel.sysctl so this host acts as the main router between all VLANs, the WireGuard network, and the WAN (untrusted) #+begin_src nix-ts :tangle modules/nixos/server/router.nix { lib, config, globals, ... }: let serviceName = "router"; + bridgeVLANs = lib.mapAttrsToList + (_: vlan: { + VLAN = vlan.id; + }) + globals.networks.home-lan.vlans; + selectVLANs = vlans: map (vlan: { VLAN = globals.networks.home-lan.vlans.${vlan}.id; }) vlans; + lan5VLANs = selectVLANs [ "home" "devices" "guests" ]; + lan4VLANs = selectVLANs [ "home" "services" ]; in { options.swarselmodules.server.${serviceName} = lib.mkEnableOption "enable ${serviceName} on server"; @@ -10440,7 +10478,68 @@ This is the configuration to make [[#h:58c7563e-6954-42e6-a622-9d06523e8e24][Hin systemd.network = { wait-online.anyInterface = true; + + netdevs = { + "10-veth" = { + netdevConfig = { + Kind = "veth"; + Name = "veth-br"; + }; + peerConfig = { + Name = "veth-int"; + }; + }; + "20-br" = { + netdevConfig = { + Kind = "bridge"; + Name = "br"; + }; + bridgeConfig = { + VLANFiltering = true; + }; + }; + }; networks = { + "40-br" = { + matchConfig.Name = "br"; + bridgeConfig = { }; + linkConfig = { + ActivationPolicy = "always-up"; + RequiredForOnline = "no"; + }; + networkConfig = { + ConfigureWithoutCarrier = true; + LinkLocalAddressing = "no"; + }; + }; + "15-veth-br" = { + matchConfig.Name = "veth-br"; + + linkConfig = { + RequiredForOnline = "no"; + }; + + networkConfig = { + Bridge = "br"; + }; + inherit bridgeVLANs; + }; + "15-veth-int" = { + matchConfig.Name = "veth-int"; + + linkConfig = { + ActivationPolicy = "always-up"; + RequiredForOnline = "no"; + }; + + networkConfig = { + ConfigureWithoutCarrier = true; + LinkLocalAddressing = "no"; + }; + + vlan = map (name: "vlan-${name}") (builtins.attrNames globals.networks.home-lan.vlans); + }; + # br "30-lan1" = { matchConfig.MACAddress = config.repo.secrets.local.networking.networks.lan1.mac; linkConfig.RequiredForOnline = "enslaved"; @@ -10448,7 +10547,9 @@ This is the configuration to make [[#h:58c7563e-6954-42e6-a622-9d06523e8e24][Hin Bridge = "br"; ConfigureWithoutCarrier = true; }; + inherit bridgeVLANs; }; + # wifi "30-lan2" = { matchConfig.MACAddress = config.repo.secrets.local.networking.networks.lan2.mac; linkConfig.RequiredForOnline = "enslaved"; @@ -10456,7 +10557,9 @@ This is the configuration to make [[#h:58c7563e-6954-42e6-a622-9d06523e8e24][Hin Bridge = "br"; ConfigureWithoutCarrier = true; }; + inherit bridgeVLANs; }; + # summers "30-lan3" = { matchConfig.MACAddress = config.repo.secrets.local.networking.networks.lan3.mac; linkConfig.RequiredForOnline = "enslaved"; @@ -10464,7 +10567,9 @@ This is the configuration to make [[#h:58c7563e-6954-42e6-a622-9d06523e8e24][Hin Bridge = "br"; ConfigureWithoutCarrier = true; }; + inherit bridgeVLANs; }; + # winters "30-lan4" = { matchConfig.MACAddress = config.repo.secrets.local.networking.networks.lan4.mac; linkConfig.RequiredForOnline = "enslaved"; @@ -10472,7 +10577,9 @@ This is the configuration to make [[#h:58c7563e-6954-42e6-a622-9d06523e8e24][Hin Bridge = "br"; ConfigureWithoutCarrier = true; }; + bridgeVLANs = lan4VLANs; }; + # lr "30-lan5" = { matchConfig.MACAddress = config.repo.secrets.local.networking.networks.lan5.mac; linkConfig.RequiredForOnline = "enslaved"; @@ -10480,10 +10587,31 @@ This is the configuration to make [[#h:58c7563e-6954-42e6-a622-9d06523e8e24][Hin Bridge = "br"; ConfigureWithoutCarrier = true; }; + bridgeVLANs = lan5VLANs; }; - }; - }; + } // lib.flip lib.concatMapAttrs globals.networks.home-lan.vlans ( + vlanName: vlanCfg: { + "40-me-${vlanName}" = lib.mkForce { + address = [ + vlanCfg.hosts.${config.node.name}.cidrv4 + vlanCfg.hosts.${config.node.name}.cidrv6 + ]; + matchConfig.Name = "me-${vlanName}"; + networkConfig = { + IPv4Forwarding = "yes"; + IPv6PrivacyExtensions = "yes"; + IPv6SendRA = true; + IPv6AcceptRA = false; + }; + ipv6Prefixes = [ + { Prefix = vlanCfg.cidrv6; } + ]; + linkConfig.RequiredForOnline = "routable"; + }; + } + ); + }; }; } @@ -16036,79 +16164,85 @@ This is the dhcp config that runs on my router. { lib, config, globals, confLib, ... }: let inherit (confLib.gen { name = "kea"; dir = "/var/lib/private/kea"; }) serviceName serviceDir; - in - { - options = { - swarselmodules.server.${serviceName} = lib.mkEnableOption "enable ${serviceName} on server"; - }; - config = lib.mkIf config.swarselmodules.server.${serviceName} { - - environment.persistence."/persist".directories = lib.mkIf config.swarselsystems.isImpermanence [ - { directory = serviceDir; mode = "0700"; } - ]; - - services.kea.dhcp4 = { - enable = true; - settings = { - lease-database = { - name = "/var/lib/kea/dhcp4.leases"; - persist = true; - type = "memfile"; - }; - valid-lifetime = 86400; - renew-timer = 3600; - interfaces-config = { - # XXX: BUG: why does this bind other macvtaps? - interfaces = map (name: "me-${name}") (builtins.attrNames globals.networks.home-lan.vlans); - service-sockets-max-retries = -1; - }; - subnet4 = lib.flip lib.mapAttrsToList globals.networks.home-lan.vlans ( - vlanName: vlanCfg: { - inherit (vlanCfg) id; - interface = "me-${vlanName}"; - subnet = vlanCfg.cidrv4; - pools = [ - { - pool = "${lib.net.cidr.host 20 vlanCfg.cidrv4} - ${lib.net.cidr.host (-6) vlanCfg.cidrv4}"; - } - ]; - option-data = - [ - { - name = "routers"; - data = vlanCfg.hosts.hintbooth.ipv4; # FIXME: how to advertise v6 address also? - } - ]; - # Advertise DNS server for VLANS that have internet access - # ++ - # lib.optional - # (lib.elem vlanName [ - # "services" - # "home" - # "devices" - # "guests" - # ]) - # { - # name = "domain-name-servers"; - # data = globals.networks.home-lan.vlans.services.hosts.hintbooth-adguardhome.ipv4; - # }; - reservations = lib.concatLists ( - lib.forEach (builtins.attrValues vlanCfg.hosts) ( - hostCfg: - lib.optional (hostCfg.mac != null) { - hw-address = hostCfg.mac; - ip-address = hostCfg.ipv4; - } - ) - ); - } - ); + dhcpX = intX: + let + x = builtins.toString intX; + in + { + enable = true; + settings = { + lease-database = { + name = "/var/lib/kea/dhcp${x}.leases"; + persist = true; + type = "memfile"; }; + valid-lifetime = 86400; + renew-timer = 3600; + interfaces-config = { + interfaces = map (name: "me-${name}") (builtins.attrNames globals.networks.home-lan.vlans); + service-sockets-max-retries = -1; + }; + "subnet${x}" = lib.flip lib.mapAttrsToList globals.networks.home-lan.vlans ( + vlanName: vlanCfg: { + inherit (vlanCfg) id; + interface = "me-${vlanName}"; + subnet = vlanCfg."cidrv${x}"; + pools = [ + { + pool = "${lib.net.cidr.host 20 vlanCfg."cidrv${x}"} - ${lib.net.cidr.host (-6) vlanCfg."cidrv${x}"}"; + } + ]; + option-data = + lib.optional (intX == 4) + { + name = "routers"; + data = vlanCfg.hosts.hintbooth."ipv${x}"; # FIXME: how to advertise v6 address also? + }; + # Advertise DNS server for VLANS that have internet access + # ++ + # lib.optional + # (lib.elem vlanName [ + # "services" + # "home" + # "devices" + # "guests" + # ]) + # { + # name = "domain-name-servers"; + # data = globals.networks.home-lan.vlans.services.hosts.hintbooth-adguardhome.ipv4; + # }; + reservations = lib.concatLists ( + lib.forEach (builtins.attrValues vlanCfg.hosts) ( + hostCfg: + lib.optional (hostCfg.mac != null) { + hw-address = hostCfg.mac; + ip-address = lib.mkIf (intX == 4) hostCfg."ipv${x}"; + ip-addresses = lib.mkIf (intX == 6) [ hostCfg."ipv${x}" ]; + } + ) + ); + } + ); }; - - }; - } + in + { + options = { + swarselmodules.server.${serviceName} = lib.mkEnableOption "enable ${serviceName} on server"; + }; + config = lib.mkIf config.swarselmodules.server.${serviceName} { + + environment.persistence."/persist".directories = lib.mkIf config.swarselsystems.isImpermanence [ + { directory = serviceDir; mode = "0700"; } + ]; + + services.kea = { + dhcp4 = dhcpX 4; + dhcp6 = dhcpX 6; + }; + + }; + } #+end_src **** nftables (firewall) :PROPERTIES: @@ -17323,15 +17457,17 @@ Some standard options that should be set vor every microvm guest. We set the def #+end_src -**** systemd-networkd (server) +**** systemd-networkd (base) :PROPERTIES: -:CUSTOM_ID: h:12370671-7892-4a74-a804-84f871acde06 +:CUSTOM_ID: h:99bf6c0e-2566-4a50-b219-fb6a7d4fb2cd :END: -Some standard options that should be set vor every microvm guest. We set the default +This set of options enables the network of the system to be managed by =systemd-networkd=: + - =networking.useNetworkd= has the effect that options from =networking.*= are not performed using network scripts but rather using =systemd-networkd=. + - =systemd.network.enable= enables the actual management of networks using the =systemd-networkd= interface. -#+begin_src nix-ts :tangle modules/nixos/optional/systemd-networkd-server.nix - { lib, config, globals, ... }: +#+begin_src nix-ts :tangle modules/nixos/optional/systemd-networkd-base.nix + { lib, config, ... }: { networking = { useDHCP = lib.mkForce false; @@ -17341,21 +17477,47 @@ Some standard options that should be set vor every microvm guest. We set the def config.repo.secrets.local.networking.networks or { } ); }; - boot.initrd.systemd.network = { + + systemd.network.enable = true; + } + +#+end_src + +**** systemd-networkd (server base) +:PROPERTIES: +:CUSTOM_ID: h:12370671-7892-4a74-a804-84f871acde06 +:END: + +Some standard options that should be set vor every microvm guest. We set the default + +#+begin_src nix-ts :tangle modules/nixos/optional/systemd-networkd-server.nix + { self, lib, config, globals, ... }: + let + inherit (config.swarselsystems) isCrypted localVLANs; + inherit (globals.general) routerServer; + + isRouter = config.node.name == routerServer; + ifName = config.swarselsystems.server.localNetwork; + in + { + imports = [ + "${self}/modules/nixos/optional/systemd-networkd-base.nix" + ]; + + boot.initrd.systemd.network = lib.mkIf (isCrypted && ((localVLANs == [ ]) || isRouter)) { enable = true; - networks."10-${config.swarselsystems.server.localNetwork}" = config.systemd.network.networks."10-${config.swarselsystems.server.localNetwork}"; + networks."10-${ifName}" = config.systemd.network.networks."10-${ifName}"; }; systemd = { network = { - enable = true; wait-online.enable = false; networks = let netConfig = config.repo.secrets.local.networking; in { - "10-${config.swarselsystems.server.localNetwork}" = { + "10-${ifName}" = lib.mkIf (isRouter || (localVLANs == [ ])) { address = [ "${globals.networks.${config.swarselsystems.server.netConfigName}.hosts.${config.node.name}.cidrv4}" "${globals.networks.${config.swarselsystems.server.netConfigName}.hosts.${config.node.name}.cidrv6}" @@ -17384,136 +17546,158 @@ Some standard options that should be set vor every microvm guest. We set the def #+end_src -**** systemd-networkd (vlans/microvms) +**** TODO systemd-networkd (server home) :PROPERTIES: -:CUSTOM_ID: h:9e1b68cb-f482-417d-8be8-48bfbf3a0d99 +:CUSTOM_ID: h:049fc27e-a28f-4ff0-b5f0-d81401bdd56f :END: -This sets up the networking framework that is needed for a server that hosts microvms. +This sets up the networking framework that is needed for a server that manages its VLAN interfaces using =systemd-networkd=. A host that is not both in the home network and using VLANs should rather be using [[#h:12370671-7892-4a74-a804-84f871acde06][systemd-networkd (server base)]] only. -The general idea is as follows: -- A host has =n= physical interfaces, which bind to the =br= bridge. Also bound to the bridge is the =veth= interfaces that the vlans are applied to. This makes it so that the macvlan interfaces still get an IP even if the physical interfaces have no carrier. -- For each VLAN defined in globals we create a VLAN here - we also create a macvlan interface for the hosts which is bound to the respective VLAN interface; also binding to that VLAN interface are the macvtap devices that are being created by the microvm module. - - normally, a guest using macvtap is not reachable by the host unless using a switch that supports hairpin-mode. However, consumers of the same VLAN can still communicate, which is realized using the macvlan interface. +We will differentiate between a host that uses microvms versus a host that is not using them. + +For a host with microvms the general idea is as follows: +- For each local VLAN we create a VLAN here - we also create a macvlan interface for the hosts which is bound to the respective VLAN interface; also binding to that VLAN interface are the macvtap devices that are being created by the microvm module. + - normally, a guest using macvtap is not reachable by the host unless using a switch that supports hairpin-mode. However, consumers of the same VLAN can still communicate, which is realized using the macvlan =me-*= interface. - even then, the kernel will only route requests when the underlying interface is up. In the case that no physical ports are used, this means that the bridge interface would effectively not work (even when administratively set to UP using =activationPolicy=) - the aforementioned =veth= takes care of that problem. + - this is really only a consideration for the [[#h:b54f2bbb-0088-46b2-957d-fd8234b772c3][Router]] (because if the interface to the router is missing on the hosts, there will be no connectivity anyways) and is hence implemented there -#+begin_src nix-ts :tangle modules/nixos/optional/systemd-networkd-vlan.nix - { lib, config, globals, ... }: - { +The principle is the same for a host without microvms, but we do not need the local =me-*= interfaces and can ignore =macvtap= config. - systemd.network = { - wait-online.anyInterface = true; - netdevs = { - "10-veth" = { - netdevConfig = { - Kind = "veth"; - Name = "veth-br"; - }; - peerConfig = { - Name = "veth-int"; - }; - }; - "20-br" = { - netdevConfig = { - Kind = "bridge"; - Name = "br"; - }; - }; - } // lib.flip lib.concatMapAttrs globals.networks.home-lan.vlans ( - vlanName: vlanCfg: { - "30-vlan-${vlanName}" = { - netdevConfig = { - Kind = "vlan"; - Name = "vlan-${vlanName}"; +A VLAN can also be used as the initrd network - this is however disabled for the router host. For that host, we need to connect from the =FritzBox!= side in case we need to reboot it (TODO: fix interface naming lan/wan which blocks this) + + +#+begin_src nix-ts :tangle modules/nixos/optional/systemd-networkd-server-home.nix + { self,lib, config, globals, ... }: + let + inherit (globals.general) routerServer; + inherit (config.swarselsystems) withMicroVMs isCrypted initrdVLAN; + + isRouter = config.node.name == routerServer; + localVLANsList = config.swarselsystems.localVLANs; + localVLANs = lib.genAttrs localVLANsList (x: globals.networks.home-lan.vlans.${x}); + in + { + imports = [ + "${self}/modules/nixos/optional/systemd-networkd-server.nix" + ]; + config = { + assertions = [ + { + assertion = ((localVLANsList != []) && (initrdVLAN != null)) || (localVLANsList == []) || (!isCrypted); + message = "This host uses VLANs and disk encryption, thus a VLAN must be specified for initrd or disk encryption must be removed."; + } + ]; + + boot.initrd = lib.mkIf (isCrypted && (localVLANsList != []) && (!isRouter)) { + availableKernelModules = [ "8021q" ]; + systemd.network = { + enable = true; + netdevs."30-vlan-${initrdVLAN}" = { + netdevConfig = { + Kind = "vlan"; + Name = "vlan-${initrdVLAN}"; + }; + vlanConfig.Id = globals.networks.home-lan.vlans.${initrdVLAN}.id; }; - vlanConfig.Id = vlanCfg.id; - }; - "40-me-${vlanName}" = { - netdevConfig = { - Name = "me-${vlanName}"; - Kind = "macvlan"; + networks = { + "10-lan" = { + matchConfig.Name = "lan"; + # This interface should only be used from attached vlans. + # So don't acquire a link local address and only wait for + # this interface to gain a carrier. + networkConfig.LinkLocalAddressing = "no"; + linkConfig.RequiredForOnline = "carrier"; + vlan = [ "vlan-${initrdVLAN}" ]; + }; + "30-vlan-${initrdVLAN}" = { + address = [ + globals.networks.home-lan.vlans.${initrdVLAN}.hosts.${config.node.name}.cidrv4 + globals.networks.home-lan.vlans.${initrdVLAN}.hosts.${config.node.name}.cidrv6 + ]; + matchConfig.Name = "vlan-${initrdVLAN}"; + networkConfig = { + IPv6PrivacyExtensions = "yes"; + }; + linkConfig.RequiredForOnline = "routable"; + }; }; - extraConfig = '' - [MACVLAN] - Mode=bridge - ''; - }; - } - ); - networks = { - "40-br" = { - matchConfig.Name = "br"; - bridgeConfig = { }; - linkConfig = { - ActivationPolicy = "always-up"; - RequiredForOnline = "no"; - }; - networkConfig = { - ConfigureWithoutCarrier = true; - LinkLocalAddressing = "no"; }; }; - "15-veth-br" = { - matchConfig.Name = "veth-br"; - linkConfig = { - RequiredForOnline = "no"; - }; - - networkConfig = { - Bridge = "br"; - }; - }; - "15-veth-int" = { - matchConfig.Name = "veth-int"; - - linkConfig = { - ActivationPolicy = "always-up"; - RequiredForOnline = "no"; - }; - - networkConfig = { - ConfigureWithoutCarrier = true; - LinkLocalAddressing = "no"; - }; - - vlan = map (name: "vlan-${name}") (builtins.attrNames globals.networks.home-lan.vlans); - }; - "90-macvtap-ignore" = { - matchConfig.Kind = "macvtap"; - linkConfig.ActivationPolicy = "manual"; - linkConfig.Unmanaged = "yes"; - }; - } // lib.flip lib.concatMapAttrs globals.networks.home-lan.vlans ( - vlanName: vlanCfg: { - "30-vlan-${vlanName}" = { - matchConfig.Name = "vlan-${vlanName}"; - networkConfig.LinkLocalAddressing = "no"; - networkConfig.MACVLAN = "me-${vlanName}"; - linkConfig.RequiredForOnline = "no"; - }; - "40-me-${vlanName}" = { - address = [ - vlanCfg.hosts.${config.node.name}.cidrv4 - vlanCfg.hosts.${config.node.name}.cidrv6 - ]; - matchConfig.Name = "me-${vlanName}"; - networkConfig = { - IPv4Forwarding = "yes"; - IPv6PrivacyExtensions = "yes"; - IPv6SendRA = true; - IPv6AcceptRA = false; + systemd.network = { + netdevs = lib.flip lib.concatMapAttrs localVLANs ( + vlanName: vlanCfg: { + "30-vlan-${vlanName}" = { + netdevConfig = { + Kind = "vlan"; + Name = "vlan-${vlanName}"; + }; + vlanConfig.Id = vlanCfg.id; + }; + # Create a MACVTAP for ourselves too, so that we can communicate with + # our guests on the same interface. + "40-me-${vlanName}" = lib.mkIf withMicroVMs { + netdevConfig = { + Name = "me-${vlanName}"; + Kind = "macvlan"; + }; + extraConfig = '' + [MACVLAN] + Mode=bridge + ''; + }; + } + ); + networks = { + "10-lan" = lib.mkIf (!isRouter) { + matchConfig.Name = "lan"; + # This interface should only be used from attached vlans. + # So don't acquire a link local address and only wait for + # this interface to gain a carrier. + networkConfig.LinkLocalAddressing = "no"; + linkConfig.RequiredForOnline = "carrier"; + vlan = (map (name: "vlan-${name}") (builtins.attrNames localVLANs)); }; - ipv6Prefixes = [ - { Prefix = vlanCfg.cidrv6; } - ]; - linkConfig.RequiredForOnline = "routable"; - }; - } - ); - }; + # Remaining macvtap interfaces should not be touched. + "90-macvtap-ignore" = lib.mkIf withMicroVMs { + matchConfig.Kind = "macvtap"; + linkConfig.ActivationPolicy = "manual"; + linkConfig.Unmanaged = "yes"; + }; + } + // lib.flip lib.concatMapAttrs localVLANs ( + vlanName: vlanCfg: + let + me = { + address = [ + vlanCfg.hosts.${config.node.name}.cidrv4 + vlanCfg.hosts.${config.node.name}.cidrv6 + ]; + gateway = lib.optionals (vlanName == "services") [ vlanCfg.hosts.${routerServer}.ipv4 vlanCfg.hosts.${routerServer}.ipv6 ]; + matchConfig.Name = "${if withMicroVMs then "me" else "vlan"}-${vlanName}"; + networkConfig.IPv6PrivacyExtensions = "yes"; + linkConfig.RequiredForOnline = "routable"; + }; - } + in + { + "30-vlan-${vlanName}" = if (!withMicroVMs) then me else { + matchConfig.Name = "vlan-${vlanName}"; + # This interface should only be used from attached macvlans. + # So don't acquire a link local address and only wait for + # this interface to gain a carrier. + networkConfig.LinkLocalAddressing = "no"; + networkConfig.MACVLAN = "me-${vlanName}"; + linkConfig.RequiredForOnline = if isRouter then "no" else "carrier"; + }; + "40-me-${vlanName}" = lib.mkIf withMicroVMs (lib.mkDefault me); + } + ); + }; + + }; + + } #+end_src @@ -21676,6 +21860,9 @@ Sets up a systemd user service for anki that does not stall the shutdown process #+end_src ***** firezone service for tray +:PROPERTIES: +:CUSTOM_ID: h:2690d49b-2b25-4204-b349-36e3efe2462a +:END: #+begin_src nix-ts :tangle modules/home/common/firezone-tray.nix { lib, config, pkgs, ... }: @@ -24344,6 +24531,18 @@ TODO: check which of these can be replaced but builtin functions. type = lib.types.str; default = ""; }; + # @ future me: dont put this under server prefix + # home-manager would then try to import all swarselsystems.server.* options + localVLANs = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ ]; + }; + # @ future me: dont put this under server prefix + # home-manager would then try to import all swarselsystems.server.* options + initrdVLAN = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + }; mainUser = lib.mkOption { type = lib.types.str; default = "swarsel"; @@ -27531,7 +27730,7 @@ This holds modules that are to be used on most hosts. These are also the most im #+end_src -* Emacse +* Emacs :PROPERTIES: :CUSTOM_ID: h:ed4cd05c-0879-41c6-bc39-3f1246a96f04 :END: diff --git a/hosts/nixos/x86_64-linux/hintbooth/default.nix b/hosts/nixos/x86_64-linux/hintbooth/default.nix index 3e3e344..20a6d6c 100644 --- a/hosts/nixos/x86_64-linux/hintbooth/default.nix +++ b/hosts/nixos/x86_64-linux/hintbooth/default.nix @@ -1,12 +1,11 @@ -{ self, config, lib, minimal, confLib, ... }: +{ self, config, lib, minimal, confLib, globals, ... }: { imports = [ ./hardware-configuration.nix ./disk-config.nix - "${self}/modules/nixos/optional/systemd-networkd-server.nix" - "${self}/modules/nixos/optional/systemd-networkd-vlan.nix" + "${self}/modules/nixos/optional/systemd-networkd-server-home.nix" ]; topology.self = { @@ -20,7 +19,10 @@ }; }; - globals.general.homeProxy = config.node.name; + globals.general = { + homeProxy = config.node.name; + routerServer = config.node.name; + }; swarselsystems = { info = "HUNSN RM02, 8GB RAM"; @@ -35,6 +37,8 @@ swapSize = "8G"; networkKernelModules = [ "igb" ]; withMicroVMs = true; + localVLANs = map (name: "${name}") (builtins.attrNames globals.networks.home-lan.vlans); + initrdVLAN = "home"; server = { wireguard.interfaces = { wgHome = { diff --git a/hosts/nixos/x86_64-linux/pyramid/default.nix b/hosts/nixos/x86_64-linux/pyramid/default.nix index 5f662f8..2884de5 100644 --- a/hosts/nixos/x86_64-linux/pyramid/default.nix +++ b/hosts/nixos/x86_64-linux/pyramid/default.nix @@ -64,7 +64,7 @@ in main = { # name = "BOE 0x0BC9 Unknown"; name = "BOE 0x0BC9"; - mode = "2560x1600"; # TEMPLATE + mode = "2560x1600"; scale = "1"; position = "2560,0"; workspace = "15:L"; @@ -78,8 +78,8 @@ in personal = true; }; - networking.nftables = { - enable = lib.mkForce false; - firewall.enable = lib.mkForce false; - }; + # networking.nftables = { + # enable = lib.mkForce false; + # firewall.enable = lib.mkForce false; + # }; } diff --git a/hosts/nixos/x86_64-linux/pyramid/disk-config.nix b/hosts/nixos/x86_64-linux/pyramid/disk-config.nix index 6feb4c8..a3e2361 100644 --- a/hosts/nixos/x86_64-linux/pyramid/disk-config.nix +++ b/hosts/nixos/x86_64-linux/pyramid/disk-config.nix @@ -75,7 +75,7 @@ fileSystems = { "/persist".neededForBoot = true; "/home".neededForBoot = true; - "/".neededForBoot = true; + "/".neededForBoot = true; # this is ok because this is not a impermanence host "/var/log".neededForBoot = true; }; } diff --git a/modules/nixos/optional/systemd-networkd-base.nix b/modules/nixos/optional/systemd-networkd-base.nix new file mode 100644 index 0000000..5081e69 --- /dev/null +++ b/modules/nixos/optional/systemd-networkd-base.nix @@ -0,0 +1,13 @@ +{ lib, config, ... }: +{ + networking = { + useDHCP = lib.mkForce false; + useNetworkd = true; + dhcpcd.enable = false; + renameInterfacesByMac = lib.mapAttrs (_: v: if (v ? mac) then v.mac else "") ( + config.repo.secrets.local.networking.networks or { } + ); + }; + + systemd.network.enable = true; +} diff --git a/modules/nixos/optional/systemd-networkd-server-home.nix b/modules/nixos/optional/systemd-networkd-server-home.nix new file mode 100644 index 0000000..9b6ef50 --- /dev/null +++ b/modules/nixos/optional/systemd-networkd-server-home.nix @@ -0,0 +1,131 @@ +{ self, lib, config, globals, ... }: +let + inherit (globals.general) routerServer; + inherit (config.swarselsystems) withMicroVMs isCrypted initrdVLAN; + + isRouter = config.node.name == routerServer; + localVLANsList = config.swarselsystems.localVLANs; + localVLANs = lib.genAttrs localVLANsList (x: globals.networks.home-lan.vlans.${x}); +in +{ + imports = [ + "${self}/modules/nixos/optional/systemd-networkd-server.nix" + ]; + config = { + assertions = [ + { + assertion = ((localVLANsList != [ ]) && (initrdVLAN != null)) || (localVLANsList == [ ]) || (!isCrypted); + message = "This host uses VLANs and disk encryption, thus a VLAN must be specified for initrd or disk encryption must be removed."; + } + ]; + + boot.initrd = lib.mkIf (isCrypted && (localVLANsList != [ ]) && (!isRouter)) { + availableKernelModules = [ "8021q" ]; + systemd.network = { + enable = true; + netdevs."30-vlan-${initrdVLAN}" = { + netdevConfig = { + Kind = "vlan"; + Name = "vlan-${initrdVLAN}"; + }; + vlanConfig.Id = globals.networks.home-lan.vlans.${initrdVLAN}.id; + }; + networks = { + "10-lan" = { + matchConfig.Name = "lan"; + # This interface should only be used from attached vlans. + # So don't acquire a link local address and only wait for + # this interface to gain a carrier. + networkConfig.LinkLocalAddressing = "no"; + linkConfig.RequiredForOnline = "carrier"; + vlan = [ "vlan-${initrdVLAN}" ]; + }; + "30-vlan-${initrdVLAN}" = { + address = [ + globals.networks.home-lan.vlans.${initrdVLAN}.hosts.${config.node.name}.cidrv4 + globals.networks.home-lan.vlans.${initrdVLAN}.hosts.${config.node.name}.cidrv6 + ]; + matchConfig.Name = "vlan-${initrdVLAN}"; + networkConfig = { + IPv6PrivacyExtensions = "yes"; + }; + linkConfig.RequiredForOnline = "routable"; + }; + }; + }; + }; + + systemd.network = { + netdevs = lib.flip lib.concatMapAttrs localVLANs ( + vlanName: vlanCfg: { + "30-vlan-${vlanName}" = { + netdevConfig = { + Kind = "vlan"; + Name = "vlan-${vlanName}"; + }; + vlanConfig.Id = vlanCfg.id; + }; + # Create a MACVTAP for ourselves too, so that we can communicate with + # our guests on the same interface. + "40-me-${vlanName}" = lib.mkIf withMicroVMs { + netdevConfig = { + Name = "me-${vlanName}"; + Kind = "macvlan"; + }; + extraConfig = '' + [MACVLAN] + Mode=bridge + ''; + }; + } + ); + networks = { + "10-lan" = lib.mkIf (!isRouter) { + matchConfig.Name = "lan"; + # This interface should only be used from attached vlans. + # So don't acquire a link local address and only wait for + # this interface to gain a carrier. + networkConfig.LinkLocalAddressing = "no"; + linkConfig.RequiredForOnline = "carrier"; + vlan = (map (name: "vlan-${name}") (builtins.attrNames localVLANs)); + }; + # Remaining macvtap interfaces should not be touched. + "90-macvtap-ignore" = lib.mkIf withMicroVMs { + matchConfig.Kind = "macvtap"; + linkConfig.ActivationPolicy = "manual"; + linkConfig.Unmanaged = "yes"; + }; + } + // lib.flip lib.concatMapAttrs localVLANs ( + vlanName: vlanCfg: + let + me = { + address = [ + vlanCfg.hosts.${config.node.name}.cidrv4 + vlanCfg.hosts.${config.node.name}.cidrv6 + ]; + gateway = lib.optionals (vlanName == "services") [ vlanCfg.hosts.${routerServer}.ipv4 vlanCfg.hosts.${routerServer}.ipv6 ]; + matchConfig.Name = "${if withMicroVMs then "me" else "vlan"}-${vlanName}"; + networkConfig.IPv6PrivacyExtensions = "yes"; + linkConfig.RequiredForOnline = "routable"; + }; + + in + { + "30-vlan-${vlanName}" = if (!withMicroVMs) then me else { + matchConfig.Name = "vlan-${vlanName}"; + # This interface should only be used from attached macvlans. + # So don't acquire a link local address and only wait for + # this interface to gain a carrier. + networkConfig.LinkLocalAddressing = "no"; + networkConfig.MACVLAN = "me-${vlanName}"; + linkConfig.RequiredForOnline = if isRouter then "no" else "carrier"; + }; + "40-me-${vlanName}" = lib.mkIf withMicroVMs (lib.mkDefault me); + } + ); + }; + + }; + +} diff --git a/modules/nixos/optional/systemd-networkd-server.nix b/modules/nixos/optional/systemd-networkd-server.nix index 99019b2..76b2bf3 100644 --- a/modules/nixos/optional/systemd-networkd-server.nix +++ b/modules/nixos/optional/systemd-networkd-server.nix @@ -1,28 +1,30 @@ -{ lib, config, globals, ... }: +{ self, lib, config, globals, ... }: +let + inherit (config.swarselsystems) isCrypted localVLANs; + inherit (globals.general) routerServer; + + isRouter = config.node.name == routerServer; + ifName = config.swarselsystems.server.localNetwork; +in { - networking = { - useDHCP = lib.mkForce false; - useNetworkd = true; - dhcpcd.enable = false; - renameInterfacesByMac = lib.mapAttrs (_: v: if (v ? mac) then v.mac else "") ( - config.repo.secrets.local.networking.networks or { } - ); - }; - boot.initrd.systemd.network = { + imports = [ + "${self}/modules/nixos/optional/systemd-networkd-base.nix" + ]; + + boot.initrd.systemd.network = lib.mkIf (isCrypted && ((localVLANs == [ ]) || isRouter)) { enable = true; - networks."10-${config.swarselsystems.server.localNetwork}" = config.systemd.network.networks."10-${config.swarselsystems.server.localNetwork}"; + networks."10-${ifName}" = config.systemd.network.networks."10-${ifName}"; }; systemd = { network = { - enable = true; wait-online.enable = false; networks = let netConfig = config.repo.secrets.local.networking; in { - "10-${config.swarselsystems.server.localNetwork}" = { + "10-${ifName}" = lib.mkIf (isRouter || (localVLANs == [ ])) { address = [ "${globals.networks.${config.swarselsystems.server.netConfigName}.hosts.${config.node.name}.cidrv4}" "${globals.networks.${config.swarselsystems.server.netConfigName}.hosts.${config.node.name}.cidrv6}" diff --git a/modules/nixos/optional/systemd-networkd-vlan.nix b/modules/nixos/optional/systemd-networkd-vlan.nix deleted file mode 100644 index 2a3470e..0000000 --- a/modules/nixos/optional/systemd-networkd-vlan.nix +++ /dev/null @@ -1,116 +0,0 @@ -{ lib, config, globals, ... }: -{ - - systemd.network = { - wait-online.anyInterface = true; - netdevs = { - "10-veth" = { - netdevConfig = { - Kind = "veth"; - Name = "veth-br"; - }; - peerConfig = { - Name = "veth-int"; - }; - }; - "20-br" = { - netdevConfig = { - Kind = "bridge"; - Name = "br"; - }; - }; - } // lib.flip lib.concatMapAttrs globals.networks.home-lan.vlans ( - vlanName: vlanCfg: { - "30-vlan-${vlanName}" = { - netdevConfig = { - Kind = "vlan"; - Name = "vlan-${vlanName}"; - }; - vlanConfig.Id = vlanCfg.id; - }; - "40-me-${vlanName}" = { - netdevConfig = { - Name = "me-${vlanName}"; - Kind = "macvlan"; - }; - extraConfig = '' - [MACVLAN] - Mode=bridge - ''; - }; - } - ); - networks = { - "40-br" = { - matchConfig.Name = "br"; - bridgeConfig = { }; - linkConfig = { - ActivationPolicy = "always-up"; - RequiredForOnline = "no"; - }; - networkConfig = { - ConfigureWithoutCarrier = true; - LinkLocalAddressing = "no"; - }; - }; - "15-veth-br" = { - matchConfig.Name = "veth-br"; - - linkConfig = { - RequiredForOnline = "no"; - }; - - networkConfig = { - Bridge = "br"; - }; - }; - "15-veth-int" = { - matchConfig.Name = "veth-int"; - - linkConfig = { - ActivationPolicy = "always-up"; - RequiredForOnline = "no"; - }; - - networkConfig = { - ConfigureWithoutCarrier = true; - LinkLocalAddressing = "no"; - }; - - vlan = map (name: "vlan-${name}") (builtins.attrNames globals.networks.home-lan.vlans); - }; - "90-macvtap-ignore" = { - matchConfig.Kind = "macvtap"; - linkConfig.ActivationPolicy = "manual"; - linkConfig.Unmanaged = "yes"; - }; - } // lib.flip lib.concatMapAttrs globals.networks.home-lan.vlans ( - vlanName: vlanCfg: { - "30-vlan-${vlanName}" = { - matchConfig.Name = "vlan-${vlanName}"; - networkConfig.LinkLocalAddressing = "no"; - networkConfig.MACVLAN = "me-${vlanName}"; - linkConfig.RequiredForOnline = "no"; - }; - "40-me-${vlanName}" = { - address = [ - vlanCfg.hosts.${config.node.name}.cidrv4 - vlanCfg.hosts.${config.node.name}.cidrv6 - ]; - matchConfig.Name = "me-${vlanName}"; - networkConfig = { - IPv4Forwarding = "yes"; - IPv6PrivacyExtensions = "yes"; - IPv6SendRA = true; - IPv6AcceptRA = false; - }; - ipv6Prefixes = [ - { Prefix = vlanCfg.cidrv6; } - ]; - linkConfig.RequiredForOnline = "routable"; - }; - } - ); - }; - -} diff --git a/modules/nixos/server/disk-encrypt.nix b/modules/nixos/server/disk-encrypt.nix index 9c0da25..943077c 100644 --- a/modules/nixos/server/disk-encrypt.nix +++ b/modules/nixos/server/disk-encrypt.nix @@ -4,6 +4,9 @@ let subnetMask = globals.networks.${config.swarselsystems.server.netConfigName}.subnetMask4; gatewayIp = globals.hosts.${config.node.name}.defaultGateway4; + inherit (globals.general) routerServer; + isRouter = config.node.name == routerServer; + hostKeyPathBase = "/etc/secrets/initrd/ssh_host_ed25519_key"; hostKeyPath = if config.swarselsystems.isImpermanence then @@ -42,7 +45,7 @@ in }; boot = lib.mkIf (!config.swarselsystems.isClient) { - kernelParams = lib.mkIf (!config.swarselsystems.isCloud) [ + kernelParams = lib.mkIf (!config.swarselsystems.isCloud && ((config.swarselsystems.localVLANs == [ ]) || isRouter)) [ "ip=${localIp}::${gatewayIp}:${subnetMask}:${config.networking.hostName}::none" ]; initrd = { diff --git a/modules/nixos/server/kea.nix b/modules/nixos/server/kea.nix index 01074fa..4fc3dfa 100644 --- a/modules/nixos/server/kea.nix +++ b/modules/nixos/server/kea.nix @@ -1,49 +1,40 @@ { lib, config, globals, confLib, ... }: let inherit (confLib.gen { name = "kea"; dir = "/var/lib/private/kea"; }) serviceName serviceDir; -in -{ - options = { - swarselmodules.server.${serviceName} = lib.mkEnableOption "enable ${serviceName} on server"; - }; - config = lib.mkIf config.swarselmodules.server.${serviceName} { - - environment.persistence."/persist".directories = lib.mkIf config.swarselsystems.isImpermanence [ - { directory = serviceDir; mode = "0700"; } - ]; - - services.kea.dhcp4 = { + dhcpX = intX: + let + x = builtins.toString intX; + in + { enable = true; settings = { lease-database = { - name = "/var/lib/kea/dhcp4.leases"; + name = "/var/lib/kea/dhcp${x}.leases"; persist = true; type = "memfile"; }; valid-lifetime = 86400; renew-timer = 3600; interfaces-config = { - # XXX: BUG: why does this bind other macvtaps? interfaces = map (name: "me-${name}") (builtins.attrNames globals.networks.home-lan.vlans); service-sockets-max-retries = -1; }; - subnet4 = lib.flip lib.mapAttrsToList globals.networks.home-lan.vlans ( + "subnet${x}" = lib.flip lib.mapAttrsToList globals.networks.home-lan.vlans ( vlanName: vlanCfg: { inherit (vlanCfg) id; interface = "me-${vlanName}"; - subnet = vlanCfg.cidrv4; + subnet = vlanCfg."cidrv${x}"; pools = [ { - pool = "${lib.net.cidr.host 20 vlanCfg.cidrv4} - ${lib.net.cidr.host (-6) vlanCfg.cidrv4}"; + pool = "${lib.net.cidr.host 20 vlanCfg."cidrv${x}"} - ${lib.net.cidr.host (-6) vlanCfg."cidrv${x}"}"; } ]; option-data = - [ + lib.optional (intX == 4) { name = "routers"; - data = vlanCfg.hosts.hintbooth.ipv4; # FIXME: how to advertise v6 address also? - } - ]; + data = vlanCfg.hosts.hintbooth."ipv${x}"; # FIXME: how to advertise v6 address also? + }; # Advertise DNS server for VLANS that have internet access # ++ # lib.optional @@ -62,7 +53,8 @@ in hostCfg: lib.optional (hostCfg.mac != null) { hw-address = hostCfg.mac; - ip-address = hostCfg.ipv4; + ip-address = lib.mkIf (intX == 4) hostCfg."ipv${x}"; + ip-addresses = lib.mkIf (intX == 6) [ hostCfg."ipv${x}" ]; } ) ); @@ -70,7 +62,21 @@ in ); }; }; +in +{ + options = { + swarselmodules.server.${serviceName} = lib.mkEnableOption "enable ${serviceName} on server"; + }; + config = lib.mkIf config.swarselmodules.server.${serviceName} { + environment.persistence."/persist".directories = lib.mkIf config.swarselsystems.isImpermanence [ + { directory = serviceDir; mode = "0700"; } + ]; + + services.kea = { + dhcp4 = dhcpX 4; + dhcp6 = dhcpX 6; + }; }; } diff --git a/modules/nixos/server/network.nix b/modules/nixos/server/network.nix index 9172d54..064a509 100644 --- a/modules/nixos/server/network.nix +++ b/modules/nixos/server/network.nix @@ -2,7 +2,6 @@ let netConfig = config.repo.secrets.local.networking; netPrefix = "${if config.swarselsystems.isCloud then config.node.name else "home"}"; - # netName = "${netPrefix}-${config.swarselsystems.server.localNetwork}"; in { options = { @@ -28,11 +27,6 @@ in swarselsystems.server.localNetwork = netConfig.localNetwork or ""; - # globals.networks.${netName}.hosts.${config.node.name} = { - # inherit (netConfig.networks.${netConfig.localNetwork}) id; - # mac = netConfig.networks.${netConfig.localNetwork}.mac or null; - # }; - globals.networks = lib.mapAttrs' (netName: _: lib.nameValuePair "${netPrefix}-${netName}" { @@ -45,7 +39,8 @@ in netConfig.networks; globals.hosts.${config.node.name} = { - inherit (config.repo.secrets.local.networking) defaultGateway4; + defaultGateway4 = netConfig.defaultGateway4 or null; + defaultGateway6 = netConfig.defaultGateway6 or null; wanAddress4 = netConfig.wanAddress4 or null; wanAddress6 = netConfig.wanAddress6 or null; isHome = if (netPrefix == "home") then true else false; diff --git a/modules/nixos/server/router.nix b/modules/nixos/server/router.nix index 51311be..9f71f71 100644 --- a/modules/nixos/server/router.nix +++ b/modules/nixos/server/router.nix @@ -1,6 +1,14 @@ { lib, config, globals, ... }: let serviceName = "router"; + bridgeVLANs = lib.mapAttrsToList + (_: vlan: { + VLAN = vlan.id; + }) + globals.networks.home-lan.vlans; + selectVLANs = vlans: map (vlan: { VLAN = globals.networks.home-lan.vlans.${vlan}.id; }) vlans; + lan5VLANs = selectVLANs [ "home" "devices" "guests" ]; + lan4VLANs = selectVLANs [ "home" "services" ]; in { options.swarselmodules.server.${serviceName} = lib.mkEnableOption "enable ${serviceName} on server"; @@ -93,7 +101,68 @@ in systemd.network = { wait-online.anyInterface = true; + + netdevs = { + "10-veth" = { + netdevConfig = { + Kind = "veth"; + Name = "veth-br"; + }; + peerConfig = { + Name = "veth-int"; + }; + }; + "20-br" = { + netdevConfig = { + Kind = "bridge"; + Name = "br"; + }; + bridgeConfig = { + VLANFiltering = true; + }; + }; + }; networks = { + "40-br" = { + matchConfig.Name = "br"; + bridgeConfig = { }; + linkConfig = { + ActivationPolicy = "always-up"; + RequiredForOnline = "no"; + }; + networkConfig = { + ConfigureWithoutCarrier = true; + LinkLocalAddressing = "no"; + }; + }; + "15-veth-br" = { + matchConfig.Name = "veth-br"; + + linkConfig = { + RequiredForOnline = "no"; + }; + + networkConfig = { + Bridge = "br"; + }; + inherit bridgeVLANs; + }; + "15-veth-int" = { + matchConfig.Name = "veth-int"; + + linkConfig = { + ActivationPolicy = "always-up"; + RequiredForOnline = "no"; + }; + + networkConfig = { + ConfigureWithoutCarrier = true; + LinkLocalAddressing = "no"; + }; + + vlan = map (name: "vlan-${name}") (builtins.attrNames globals.networks.home-lan.vlans); + }; + # br "30-lan1" = { matchConfig.MACAddress = config.repo.secrets.local.networking.networks.lan1.mac; linkConfig.RequiredForOnline = "enslaved"; @@ -101,7 +170,9 @@ in Bridge = "br"; ConfigureWithoutCarrier = true; }; + inherit bridgeVLANs; }; + # wifi "30-lan2" = { matchConfig.MACAddress = config.repo.secrets.local.networking.networks.lan2.mac; linkConfig.RequiredForOnline = "enslaved"; @@ -109,7 +180,9 @@ in Bridge = "br"; ConfigureWithoutCarrier = true; }; + inherit bridgeVLANs; }; + # summers "30-lan3" = { matchConfig.MACAddress = config.repo.secrets.local.networking.networks.lan3.mac; linkConfig.RequiredForOnline = "enslaved"; @@ -117,7 +190,9 @@ in Bridge = "br"; ConfigureWithoutCarrier = true; }; + inherit bridgeVLANs; }; + # winters "30-lan4" = { matchConfig.MACAddress = config.repo.secrets.local.networking.networks.lan4.mac; linkConfig.RequiredForOnline = "enslaved"; @@ -125,7 +200,9 @@ in Bridge = "br"; ConfigureWithoutCarrier = true; }; + bridgeVLANs = lan4VLANs; }; + # lr "30-lan5" = { matchConfig.MACAddress = config.repo.secrets.local.networking.networks.lan5.mac; linkConfig.RequiredForOnline = "enslaved"; @@ -133,10 +210,31 @@ in Bridge = "br"; ConfigureWithoutCarrier = true; }; + bridgeVLANs = lan5VLANs; }; - }; - }; + } // lib.flip lib.concatMapAttrs globals.networks.home-lan.vlans ( + vlanName: vlanCfg: { + "40-me-${vlanName}" = lib.mkForce { + address = [ + vlanCfg.hosts.${config.node.name}.cidrv4 + vlanCfg.hosts.${config.node.name}.cidrv6 + ]; + matchConfig.Name = "me-${vlanName}"; + networkConfig = { + IPv4Forwarding = "yes"; + IPv6PrivacyExtensions = "yes"; + IPv6SendRA = true; + IPv6AcceptRA = false; + }; + ipv6Prefixes = [ + { Prefix = vlanCfg.cidrv6; } + ]; + linkConfig.RequiredForOnline = "routable"; + }; + } + ); + }; }; } diff --git a/modules/shared/options.nix b/modules/shared/options.nix index 5d3f169..28f2e2e 100644 --- a/modules/shared/options.nix +++ b/modules/shared/options.nix @@ -37,6 +37,18 @@ type = lib.types.str; default = ""; }; + # @ future me: dont put this under server prefix + # home-manager would then try to import all swarselsystems.server.* options + localVLANs = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ ]; + }; + # @ future me: dont put this under server prefix + # home-manager would then try to import all swarselsystems.server.* options + initrdVLAN = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + }; mainUser = lib.mkOption { type = lib.types.str; default = "swarsel"; diff --git a/secrets/repo/globals.nix.enc b/secrets/repo/globals.nix.enc index bcead07..22b6467 100644 --- a/secrets/repo/globals.nix.enc +++ b/secrets/repo/globals.nix.enc @@ -1,5 +1,5 @@ { - "data": "ENC[AES256_GCM,data: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,iv:Fep8PNrxh18J4n8gYLbmJ2+Jn7kD/4KCLs11GDDfshM=,tag:UwkE59mcIslEcrqNQ0qCTw==,type:str]", + "data": "ENC[AES256_GCM,data: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,iv:LMwQRM3y2kJYM+9h7G9Cr2iwiQZx1KNmIn0zIa3MHRk=,tag:BKXIvu8hwZktmHqJ8kpeiw==,type:str]", "sops": { "age": [ { @@ -51,8 +51,8 @@ "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAvbTFqR3RScWxnTFR3dlhv\nNUNEVGRkUDB4L3J6ckJIMWZCWk44RjdpVmxRCkhzZTBuSGduanBmQWsvNW5XMWQv\nYVZmNS9FVzN3a05kUVBheDljYUUrcHcKLS0tIGxPN1dWVkg4NUpnZGJ6VWFJWFVZ\nYnNvRG94MmFxYnlDQ0JyeDNFQldzdlkKsp+nYSR6Lxq8b3/dpMO7uTbNnO0Bva7w\nb9s908PLaZEN1jywEoba3yq743vuEHCKQWFIfDtsRcbNR6Yr4d2eGw==\n-----END AGE ENCRYPTED FILE-----\n" } ], - "lastmodified": "2025-12-24T23:33:32Z", - "mac": "ENC[AES256_GCM,data:mrwZUGeMtgAqSd2v387rGaf0bHZDC1IjIUlFPzB3Tj7CHE9Li4vAp7f9Bvd6v62eNcuwrnW8UoXju63fdDLK7l3EczdWsA/EJrvxIr29x+NByzfbUtShexg24mwsN7HMBnhyvil1CF4PMN4EmxxOn05cXOsAqln38IEPY6n6+SM=,iv:7PeUcjVA5u1KXsBZv+UrVQaCqEjXqY6wnELDEeHyQ9c=,tag:A+bETVg1tlHZlAm0rJ9Z8A==,type:str]", + "lastmodified": "2026-01-02T00:59:46Z", + "mac": "ENC[AES256_GCM,data:NJk6fM55PTbWRIfOyTGaHVHCiQeDQcp/V5QxGkf0E7/+u4aUIyWhrkHCORmv4dLmI+bfPBiGOyWWvQz7NqN8Cx40M4e+F7nkvAuDGGnIY8gTw+Qt+FYwSbxcFJBmTQ1wyw99xhvpKzl10uJsP8w3wqXEntC4bLQR9MDC7LlVeTY=,iv:p9MZvnheBxGT+EoZjboDLWaxlSWaMHTTADgPX+uDU5k=,tag:wsiEkpQrE88x/WZM7DhsFg==,type:str]", "pgp": [ { "created_at": "2025-12-15T21:53:38Z",