From c4782f15c525043250288c62d036dc64d2592d73 Mon Sep 17 00:00:00 2001 From: Elias Coppens Date: Sat, 1 Mar 2025 15:59:59 +0100 Subject: [PATCH] feat: add test AP Signed-off-by: Elias Coppens --- hive.nix | 7 +- machines/liminix/ap-test/_configuration.nix | 46 ++++++++ machines/liminix/ap-test/addresses.nix | 24 ++++ machines/liminix/ap-test/dns.nix | 25 ++++ machines/liminix/ap-test/ipc.nix | 12 ++ machines/liminix/ap-test/lan.nix | 20 ++++ machines/liminix/ap-test/management.nix | 20 ++++ machines/liminix/ap-test/metadata.nix | 19 ++++ machines/liminix/ap-test/recovery.nix | 68 +++++++++++ machines/liminix/ap-test/system.nix | 33 ++++++ machines/liminix/ap-test/wlan.nix | 120 ++++++++++++++++++++ meta/nodes/liminix.nix | 16 ++- 12 files changed, 408 insertions(+), 2 deletions(-) create mode 100644 machines/liminix/ap-test/_configuration.nix create mode 100644 machines/liminix/ap-test/addresses.nix create mode 100644 machines/liminix/ap-test/dns.nix create mode 100644 machines/liminix/ap-test/ipc.nix create mode 100644 machines/liminix/ap-test/lan.nix create mode 100644 machines/liminix/ap-test/management.nix create mode 100644 machines/liminix/ap-test/metadata.nix create mode 100644 machines/liminix/ap-test/recovery.nix create mode 100644 machines/liminix/ap-test/system.nix create mode 100644 machines/liminix/ap-test/wlan.nix diff --git a/hive.nix b/hive.nix index 666c79d..dd4ca6b 100644 --- a/hive.nix +++ b/hive.nix @@ -116,7 +116,12 @@ in # Import the default modules imports = [ # Import the base configuration for each node - ./machines/liminix/ap-v01/_configuration.nix + ( + if name != "ap-test" then + ./machines/liminix/ap-v01/_configuration.nix + else + ./machines/liminix/ap-test/_configuration.nix + ) ./modules/generic ./modules/${category name} ]; diff --git a/machines/liminix/ap-test/_configuration.nix b/machines/liminix/ap-test/_configuration.nix new file mode 100644 index 0000000..6a420c5 --- /dev/null +++ b/machines/liminix/ap-test/_configuration.nix @@ -0,0 +1,46 @@ +# SPDX-FileCopyrightText: 2024 Ryan Lahfa +# +# SPDX-License-Identifier: EUPL-1.2 + +{ + modulesPath, + sourcePkgs, + name, + ... +}: +{ + imports = [ + "${modulesPath}/wlan.nix" + "${modulesPath}/network" + "${modulesPath}/hostapd" + "${modulesPath}/ssh" + "${modulesPath}/ntp" + "${modulesPath}/vlan" + "${modulesPath}/bridge" + "${modulesPath}/jitter-rng" + "${modulesPath}/pki" + "${modulesPath}/ubus" + "${modulesPath}/openwrt-prometheus-exporter" + # System-level configuration + ./system.nix + # Configures our own WLAN. + ./wlan.nix + # Configures our LAN interfaces, e.g. bridge + VLANs. + ./lan.nix + # Configures our IPv4/IPv6 addresses, e.g. DHCPv4 on VLAN 0, SLAAC on VLAN 3001. + ./addresses.nix + # Configures a basic local DNS. + ./dns.nix + # Configures our management layer, e.g. SSH server + DGNum FAI keys. + ./management.nix + # Configures our recovery system, e.g. a levitation script. + ./recovery.nix + # Metadata on the system for field recovery. + ./metadata.nix + # TODO: god that's so a fucking hack. + (import "${modulesPath}/../devices/zyxel-nwa50ax").module + ]; + + hostname = name; + nixpkgs.source = sourcePkgs.path; +} diff --git a/machines/liminix/ap-test/addresses.nix b/machines/liminix/ap-test/addresses.nix new file mode 100644 index 0000000..fb27045 --- /dev/null +++ b/machines/liminix/ap-test/addresses.nix @@ -0,0 +1,24 @@ +# SPDX-FileCopyrightText: 2024 Ryan Lahfa +# +# SPDX-License-Identifier: EUPL-1.2 + +{ config, ... }: +let + svc = config.system.service; + # FIXME switch to ipv6 tu be able to scale + adminIp = "10.0.253.253"; +in +{ + services.admin-ip = svc.network.address.build { + interface = config.services.admin-vlan; + address = adminIp; + prefixLength = 24; + family = "inet"; + }; + + services.admin-defaultroute4 = svc.network.route.build { + via = "10.0.253.1"; + target = "default"; + dependencies = [ config.services.admin-ip ]; + }; +} diff --git a/machines/liminix/ap-test/dns.nix b/machines/liminix/ap-test/dns.nix new file mode 100644 index 0000000..b371292 --- /dev/null +++ b/machines/liminix/ap-test/dns.nix @@ -0,0 +1,25 @@ +# SPDX-FileCopyrightText: 2024 Ryan Lahfa +# +# SPDX-License-Identifier: EUPL-1.2 + +{ pkgs, lib, ... }: +let + inherit (pkgs.pseudofile) dir symlink; + # TODO: imho, DNS should be static and provided by the router? + dns = [ + "8.8.8.8" + "8.8.4.4" + "1.0.0.1" + ]; + resolvconf = pkgs.writeText "resolv.conf" ( + lib.concatMapStringsSep "\n" (dns: ''echo "nameserver ${dns}" >> resolv.conf'') dns + ); +in +{ + # TODO: support dynamic reconfiguration once we are in the target VLAN? + filesystem = dir { + etc = dir { + "resolv.conf" = symlink "${resolvconf}"; + }; + }; +} diff --git a/machines/liminix/ap-test/ipc.nix b/machines/liminix/ap-test/ipc.nix new file mode 100644 index 0000000..43c918c --- /dev/null +++ b/machines/liminix/ap-test/ipc.nix @@ -0,0 +1,12 @@ +# SPDX-FileCopyrightText: 2024 Ryan Lahfa +# +# SPDX-License-Identifier: EUPL-1.2 + +{ config, ... }: +let + svc = config.system.service; +in +{ + # ubus socket for various needs. + services.ubus = svc.ubus.build { }; +} diff --git a/machines/liminix/ap-test/lan.nix b/machines/liminix/ap-test/lan.nix new file mode 100644 index 0000000..1d73ece --- /dev/null +++ b/machines/liminix/ap-test/lan.nix @@ -0,0 +1,20 @@ +# SPDX-FileCopyrightText: 2024 Ryan Lahfa +# +# SPDX-License-Identifier: EUPL-1.2 + +{ config, ... }: +let + svc = config.system.service; +in +{ + # Our bridging is a bit complicated, therefore, we need iproute2. + programs.iproute2.enable = true; + + services = { + admin-vlan = svc.vlan.build { + ifname = "admin"; + primary = config.hardware.networkInterfaces.lan; + vid = "3001"; + }; + }; +} diff --git a/machines/liminix/ap-test/management.nix b/machines/liminix/ap-test/management.nix new file mode 100644 index 0000000..0fe6d87 --- /dev/null +++ b/machines/liminix/ap-test/management.nix @@ -0,0 +1,20 @@ +# SPDX-FileCopyrightText: 2024 Ryan Lahfa +# +# SPDX-License-Identifier: EUPL-1.2 + +{ config, ... }: +let + svc = config.system.service; +in +{ + # SSH keys are handled by the access control module. + dgn-access-control.enable = true; + users.root = { + # TODO: Change this well-known password + passwd = "$6$Z2MiaMXkpUJRPl2/$fxVE3iD/n208CISM2F6OnWj0Qq0QG2tTQqLCjU80PFJJGIwNLLyOp6SeYH3dH20OvJX1loZRETrThZfIPw.rb/"; + }; + services.sshd = svc.ssh.build { allowRoot = true; }; + services.openwrt-prometheus-exporter = svc.openwrt-prometheus-exporter.build { + httpPorts = [ 9100 ]; + }; +} diff --git a/machines/liminix/ap-test/metadata.nix b/machines/liminix/ap-test/metadata.nix new file mode 100644 index 0000000..80dce50 --- /dev/null +++ b/machines/liminix/ap-test/metadata.nix @@ -0,0 +1,19 @@ +# SPDX-FileCopyrightText: 2024 Ryan Lahfa +# +# SPDX-License-Identifier: EUPL-1.2 + +{ pkgs, ... }: +let + inherit (pkgs.pseudofile) dir; +in +{ + filesystem = dir { + etc = dir { + "nixpkgs.version" = { + type = "f"; + file = "${pkgs.lib.version}"; + mode = "0444"; + }; + }; + }; +} diff --git a/machines/liminix/ap-test/recovery.nix b/machines/liminix/ap-test/recovery.nix new file mode 100644 index 0000000..35edaa7 --- /dev/null +++ b/machines/liminix/ap-test/recovery.nix @@ -0,0 +1,68 @@ +# SPDX-FileCopyrightText: 2024 Ryan Lahfa +# +# SPDX-License-Identifier: EUPL-1.2 + +{ + config, + pkgs, + modulesPath, + ... +}: +let + svc = config.system.service; + parentConfig = config; +in +{ + defaultProfile.packages = [ + # Levitate enable us to mass-reinstall the system on the fly. + # TODO: Test levitation + (pkgs.levitate.override { + config = { + imports = [ + "${modulesPath}/network" + "${modulesPath}/ssh" + "${modulesPath}/hardware.nix" + "${modulesPath}/kernel" + "${modulesPath}/outputs/tftpboot.nix" + "${modulesPath}/outputs.nix" + # FIXME: DHCP has a hidden deps on this, shoud be done in a more intelligent way upstream + "${modulesPath}/iproute2.nix" + ( + { config, ... }: + { + # FIXME: DHCP has a hidden deps on this, shoud be done in a more intelligent way upstream + programs.iproute2.enable = true; + services = { + # In this situation, we fallback to the appro VLAN but keep admin vlan. + # Simplest DHCPv4 we can find. + dhcpv4 = svc.network.dhcp.client.build { + interface = parentConfig.hardware.networkInterfaces.lan; + }; + inherit (parentConfig.services) + sshd + admin-vlan + admin-ip + admin-defaultroute4 + ; + defaultroute4 = svc.network.route.build { + via = "$(output ${config.services.dhcpv4} router)"; + target = "default"; + dependencies = [ config.services.dhcpv4 ]; + }; + }; + } + ) + ]; + hostname = "${parentConfig.hostname}-live"; + nixpkgs.buildPlatform = builtins.currentSystem; + + defaultProfile.packages = with pkgs; [ + mtdutils + zyxel-bootconfig + ]; + # Only keep root, which should inherit from DGN access control's root permissions. + users.root = config.users.root; + }; + }) + ]; +} diff --git a/machines/liminix/ap-test/system.nix b/machines/liminix/ap-test/system.nix new file mode 100644 index 0000000..0d258b8 --- /dev/null +++ b/machines/liminix/ap-test/system.nix @@ -0,0 +1,33 @@ +# SPDX-FileCopyrightText: 2024 Ryan Lahfa +# +# SPDX-License-Identifier: EUPL-1.2 + +{ pkgs, config, ... }: +let + svc = config.system.service; +in +{ + # Get moar random please + services = { + jitter = svc.jitter-rng.build { }; + packet_forwarding = svc.network.forward.build { }; + ntp = config.system.service.ntp.build { + pools = { + "pool.ntp.org" = [ "iburst" ]; + }; + + dependencies = [ config.services.jitter ]; + }; + }; + + boot.tftp = { + serverip = "192.0.2.10"; + ipaddr = "192.0.2.12"; + }; + + defaultProfile.packages = with pkgs; [ + zyxel-bootconfig + min-collect-garbage + hostapd-radius + ]; +} diff --git a/machines/liminix/ap-test/wlan.nix b/machines/liminix/ap-test/wlan.nix new file mode 100644 index 0000000..cfb562f --- /dev/null +++ b/machines/liminix/ap-test/wlan.nix @@ -0,0 +1,120 @@ +# SPDX-FileCopyrightText: 2024 Ryan Lahfa +# +# SPDX-License-Identifier: EUPL-1.2 + +{ + config, + pkgs, + lib, + ... +}: +let + svc = config.system.service; + + mac-1 = "02:5B:6A:FF:FF:FE"; + mac-2 = "02:5B:6A:FF:FF:FF"; + + channel-1 = 1; + channel-2 = 36; + + secrets-1 = { + ssid = "DGNum"; + }; + secrets-2 = { + ssid = "DGNum 5G"; + }; + baseParams = { + country_code = "FR"; + hw_mode = "g"; + channel = channel-1; + wmm_enabled = 1; + ieee80211n = 1; + ht_capab = "[LDPC][GF][HT40-][HT40+][SHORT-GI-40][MAX-AMSDU-7935][TX-STBC]"; + auth_algs = 1; + wpa = 2; + wpa_pairwise = "TKIP CCMP"; + rsn_pairwise = "CCMP"; + }; + + radiusKeyMgmt = { + wpa_key_mgmt = "WPA-EAP"; + }; + + modernParams = { + hw_mode = "a"; + he_su_beamformer = 1; + he_su_beamformee = 1; + he_mu_beamformer = 1; + preamble = 1; + # Allow radar detection. + ieee80211d = 1; + ieee80211h = 1; + ieee80211ac = 1; + ieee80211ax = 1; + vht_capab = "[MAX-MPDU-7991][SU-BEAMFORMEE][SU-BEAMFORMER][RXLDPC][SHORT-GI-80][MAX-A-MPDU-LEN-EXP3][RX-ANTENNA-PATTERN][TX-ANTENNA-PATTERN][TX-STBC-2BY1][RX-STBC-1][MU-BEAMFORMER]"; + vht_oper_chwidth = 1; + he_oper_chwidth = 1; + channel = channel-2; # TODO understand interferences + vht_oper_centr_freq_seg0_idx = channel-2 + 6; + he_oper_centr_freq_seg0_idx = channel-2 + 6; + require_vht = 1; + }; + + clientRadius = { + ieee8021x = 1; + eapol_version = 2; + use_pae_group_addr = 1; + dynamic_vlan = 3; + vlan_tagged_interface = "lan"; + }; + + externalRadius = { + # TODO: when we have proper IPAM, set the right value here. + own_ip_addr = "127.0.0.1"; + nas_identifier = "ap01.dgnum.eu"; + + # No DNS here, hostapd do not support this mode. + auth_server_addr = "129.199.195.129"; + auth_server_port = 1812; + auth_server_shared_secret = + let + secret = builtins.getEnv "RADIUS_SECRET"; + in + if secret == "" then + lib.warn "Using a dummy RADIUS secret. Please do not use in production" "DUMMYSECRET" + else + secret; + }; + + mkWifiSta = + params: interface: secrets: + svc.hostapd.build { + inherit interface; + package = pkgs.hostapd-radius; + params = params // secrets; + dependencies = [ config.services.jitter ]; + }; +in +{ + hardware.wlanMacAddresses = { + wlan0 = mac-1; + wlan1 = mac-2; + }; + services = { + # wlan0 is the 2.4GHz interface. + hostap-1 = mkWifiSta ( + baseParams // clientRadius // externalRadius // radiusKeyMgmt + ) config.hardware.networkInterfaces.wlan0 secrets-1; + hostap-1-ready = svc.hostapd-ready.build { + interface = config.hardware.networkInterfaces.wlan0; + }; + # wlan1 is the 5GHz interface, e.g. AX capable. + hostap-2 = mkWifiSta ( + baseParams // clientRadius // externalRadius // radiusKeyMgmt // modernParams + ) config.hardware.networkInterfaces.wlan1 secrets-2; + # Oneshot that waits until the hostapd has set the interface in operational state. + hostap-2-ready = svc.hostapd-ready.build { + interface = config.hardware.networkInterfaces.wlan1; + }; + }; +} diff --git a/meta/nodes/liminix.nix b/meta/nodes/liminix.nix index c421af3..e771936 100644 --- a/meta/nodes/liminix.nix +++ b/meta/nodes/liminix.nix @@ -128,5 +128,19 @@ let mkRange = { from, to }: builtins.genList (x: x + from) (to - from); in -{ } +{ + ap-test = { + site = "unknown"; + adminGroups = [ "fai" ]; + + hashedPassword = "$y$j9T$DMOQEWOYFHjNS0myrXp4x/$MG33VSdXGvib.99eN.AbvyVdNNJw4ERjAwK4.ULJe/A"; + + stateVersion = null; + + nixpkgs = { + system = "zyxel-nwa50ax"; + version = "24.05"; + }; + }; +} // builtins.foldl' (nodes: building: nodes // mkAPs-building building) { } (builtins.attrValues APs)