From 583a74d8ad7545b8791e651aea7865e5f170f44b Mon Sep 17 00:00:00 2001 From: Morgan Jones Date: Sun, 9 Feb 2025 20:46:41 -0800 Subject: [PATCH 1/5] nixos/vwifi: init module This module does not currently have its own test suite: it is intended to be used in test suites for other modules that use wifi. --- nixos/modules/module-list.nix | 1 + nixos/modules/services/networking/vwifi.nix | 200 ++++++++++++++++++++ 2 files changed, 201 insertions(+) create mode 100644 nixos/modules/services/networking/vwifi.nix diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index f9d300e3da80..478731ecaa3d 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -1352,6 +1352,7 @@ ./services/networking/veilid.nix ./services/networking/vdirsyncer.nix ./services/networking/vsftpd.nix + ./services/networking/vwifi.nix ./services/networking/wasabibackend.nix ./services/networking/websockify.nix ./services/networking/wg-access-server.nix diff --git a/nixos/modules/services/networking/vwifi.nix b/nixos/modules/services/networking/vwifi.nix new file mode 100644 index 000000000000..64b7fbdf45a4 --- /dev/null +++ b/nixos/modules/services/networking/vwifi.nix @@ -0,0 +1,200 @@ +{ + config, + lib, + pkgs, + ... +}: + +let + inherit (lib.modules) mkIf mkMerge; + inherit (lib.options) mkOption mkPackageOption mkEnableOption; + inherit (lib.lists) optional optionals; + inherit (lib.strings) + hasSuffix + escapeShellArgs + ; + inherit (lib) types; + cfg = config.services.vwifi; +in +{ + options = { + services.vwifi = + let + mkOptionalPort = + name: + mkOption { + description = '' + The ${name} port. Set to null if we should leave it unset. + ''; + type = with types; nullOr port; + default = null; + }; + in + { + package = mkPackageOption pkgs "vwifi" { }; + module = { + enable = mkEnableOption "mac80211_hwsim module"; + numRadios = mkOption { + description = "The number of virtual radio interfaces to create."; + type = types.int; + default = 1; + }; + macPrefix = mkOption { + description = '' + The prefix for MAC addresses to use, without the trailing ':'. + If one radio is created, you can specify the whole MAC address here. + The default is defined in vwifi/src/config.h. + ''; + type = types.strMatching "^(([0-9A-Fa-f]{2}:){0,5}[0-9A-Fa-f]{2})$"; + default = "74:F8:F6"; + }; + }; + client = { + enable = mkEnableOption "vwifi client"; + spy = mkEnableOption "spy mode, useful for wireless monitors"; + serverAddress = mkOption { + description = '' + The address of the server. If set to null, will try to use the vsock protocol. + Note that this assumes that the server is spawned on the host and passed through to + QEMU, with something like: + + -device vhost-vsock-pci,id=vwifi0,guest-cid=42 + ''; + type = with types; nullOr str; + default = null; + }; + serverPort = mkOptionalPort "server port"; + extraArgs = mkOption { + description = '' + Extra arguments to pass to vwifi-client. You can use this if you want to bring + the radios up using vwifi-client instead of at boot. + ''; + type = with types; listOf str; + default = [ ]; + example = [ + "--number" + "3" + ]; + }; + }; + server = { + enable = mkEnableOption "vwifi server"; + vsock.enable = mkEnableOption "vsock kernel module"; + ports = { + vhost = mkOptionalPort "vhost"; + tcp = mkOptionalPort "TCP server"; + spy = mkOptionalPort "spy interface"; + control = mkOptionalPort "control interface"; + }; + openFirewall = mkEnableOption "opening the firewall for the TCP and spy ports"; + extraArgs = mkOption { + description = '' + Extra arguments to pass to vwifi-server. You can use this for things including + changing the ports or inducing packet loss. + ''; + type = with types; listOf str; + default = [ ]; + example = [ "--lost-packets" ]; + }; + }; + }; + }; + + config = mkMerge [ + (mkIf cfg.module.enable { + boot.kernelModules = [ + "mac80211_hwsim" + ]; + boot.extraModprobeConfig = '' + # We'll add more radios using vwifi-add-interfaces in the systemd unit. + options mac80211_hwsim radios=0 + ''; + systemd.services.vwifi-add-interfaces = mkIf (cfg.module.numRadios > 0) { + description = "vwifi interface bringup"; + wantedBy = [ "network-pre.target" ]; + serviceConfig = { + Type = "oneshot"; + ExecStart = + let + args = [ + (toString cfg.module.numRadios) + cfg.module.macPrefix + ]; + in + "${cfg.package}/bin/vwifi-add-interfaces ${escapeShellArgs args}"; + }; + }; + assertions = [ + { + assertion = !(hasSuffix ":" cfg.module.macPrefix); + message = '' + services.vwifi.module.macPrefix should not have a trailing ":". + ''; + } + ]; + }) + (mkIf cfg.client.enable { + systemd.services.vwifi-client = + let + clientArgs = + optional cfg.client.spy "--spy" + ++ optional (cfg.client.serverAddress != null) cfg.client.serverAddress + ++ optionals (cfg.client.serverPort != null) [ + "--port" + cfg.client.serverPort + ] + ++ cfg.client.extraArgs; + in + rec { + description = "vwifi client"; + wantedBy = [ "multi-user.target" ]; + after = [ "network.target" ]; + requires = after; + serviceConfig = { + ExecStart = "${cfg.package}/bin/vwifi-client ${escapeShellArgs clientArgs}"; + }; + }; + }) + (mkIf cfg.server.enable { + boot.kernelModules = mkIf cfg.server.vsock.enable [ + "vhost_vsock" + ]; + networking.firewall.allowedTCPPorts = mkIf cfg.server.openFirewall ( + optional (cfg.server.ports.tcp != null) cfg.server.ports.tcp + ++ optional (cfg.server.ports.spy != null) cfg.server.ports.spy + ); + systemd.services.vwifi-server = + let + serverArgs = + optionals (cfg.server.ports.vhost != null) [ + "--port-vhost" + (toString cfg.server.ports.vhost) + ] + ++ optionals (cfg.server.ports.tcp != null) [ + "--port-tcp" + (toString cfg.server.ports.tcp) + ] + ++ optionals (cfg.server.ports.spy != null) [ + "--port-spy" + (toString cfg.server.ports.spy) + ] + ++ optionals (cfg.server.ports.control != null) [ + "--port-ctrl" + (toString cfg.server.ports.control) + ] + ++ cfg.server.extraArgs; + in + rec { + description = "vwifi server"; + wantedBy = [ "multi-user.target" ]; + after = [ "network.target" ]; + requires = after; + serviceConfig = { + ExecStart = "${cfg.package}/bin/vwifi-server ${escapeShellArgs serverArgs}"; + }; + }; + }) + ]; + + meta.maintainers = with lib.maintainers; [ numinit ]; +} From 36cddaaa6fd4f2dec9c16d220fe6ffe506841a81 Mon Sep 17 00:00:00 2001 From: Morgan Jones Date: Sun, 9 Feb 2025 20:57:07 -0800 Subject: [PATCH 2/5] nixos/kismet: init module Use vwifi to write a proper test for Kismet. This test demonstrates how to simulate wireless networks in NixOS tests, and extract meaningful data by putting an interface in monitor mode using Kismet. --- nixos/modules/module-list.nix | 1 + nixos/modules/services/networking/kismet.nix | 459 +++++++++++++++++++ nixos/tests/all-tests.nix | 1 + nixos/tests/kismet.nix | 266 +++++++++++ 4 files changed, 727 insertions(+) create mode 100644 nixos/modules/services/networking/kismet.nix create mode 100644 nixos/tests/kismet.nix diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index 478731ecaa3d..6e2373694e3c 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -1175,6 +1175,7 @@ ./services/networking/kea.nix ./services/networking/keepalived/default.nix ./services/networking/keybase.nix + ./services/networking/kismet.nix ./services/networking/knot.nix ./services/networking/kresd.nix ./services/networking/lambdabot.nix diff --git a/nixos/modules/services/networking/kismet.nix b/nixos/modules/services/networking/kismet.nix new file mode 100644 index 000000000000..4e14e9fd51d4 --- /dev/null +++ b/nixos/modules/services/networking/kismet.nix @@ -0,0 +1,459 @@ +{ + config, + lib, + pkgs, + ... +}: + +let + inherit (lib.trivial) isFloat isInt isBool; + inherit (lib.modules) mkIf; + inherit (lib.options) + literalExpression + mkOption + mkPackageOption + mkEnableOption + ; + inherit (lib.strings) + isString + escapeShellArg + escapeShellArgs + concatMapStringsSep + concatMapAttrsStringSep + replaceStrings + substring + stringLength + hasInfix + hasSuffix + typeOf + match + ; + inherit (lib.lists) all isList flatten; + inherit (lib.attrsets) + attrsToList + filterAttrs + optionalAttrs + mapAttrs' + mapAttrsToList + nameValuePair + ; + inherit (lib.generators) toKeyValue; + inherit (lib) types; + + # Deeply checks types for a given type function. Calls `override` with type and value. + deep = + func: override: type: + let + prev = func type; + in + prev + // { + check = value: prev.check value && (override type value); + }; + + # Deep listOf. + listOf' = deep types.listOf (type: value: all type.check value); + + # Deep attrsOf. + attrsOf' = deep types.attrsOf (type: value: all (item: type.check item.value) (attrsToList value)); + + # Kismet config atoms. + atom = + with types; + oneOf [ + number + bool + str + ]; + + # Composite types. + listOfAtom = listOf' atom; + atomOrList = with types; either atom listOfAtom; + lists = listOf' atomOrList; + kvPair = attrsOf' atomOrList; + kvPairs = listOf' kvPair; + + # Options that eval to a string with a header (foo:key=value) + headerKvPair = attrsOf' (attrsOf' atomOrList); + headerKvPairs = attrsOf' (listOf' (attrsOf' atomOrList)); + + # Toplevel config type. + topLevel = + let + topLevel' = + with types; + oneOf [ + headerKvPairs + headerKvPair + kvPairs + kvPair + listOfAtom + lists + atom + ]; + in + topLevel' + // { + description = "Kismet config stanza"; + }; + + # Throws invalid. + invalid = atom: throw "invalid value '${toString atom}' of type '${typeOf atom}'"; + + # Converts an atom. + mkAtom = + atom: + if isString atom then + if hasInfix "\"" atom || hasInfix "," atom then + ''"${replaceStrings [ ''"'' ] [ ''\"'' ] atom}"'' + else + atom + else if isFloat atom || isInt atom || isBool atom then + toString atom + else + invalid atom; + + # Converts an inline atom or list to a string. + mkAtomOrListInline = + atomOrList: + if isList atomOrList then + mkAtom "${concatMapStringsSep "," mkAtom atomOrList}" + else + mkAtom atomOrList; + + # Converts an out of line atom or list to a string. + mkAtomOrList = + atomOrList: + if isList atomOrList then + "${concatMapStringsSep "," mkAtomOrListInline atomOrList}" + else + mkAtom atomOrList; + + # Throws if the string matches the given regex. + deny = + regex: str: + assert (match regex str) == null; + str; + + # Converts a set of k/v pairs. + convertKv = concatMapAttrsStringSep "," ( + name: value: "${mkAtom (deny "=" name)}=${mkAtomOrListInline value}" + ); + + # Converts k/v pairs with a header. + convertKvWithHeader = header: attrs: "${mkAtom (deny ":" header)}:${convertKv attrs}"; + + # Converts the entire config. + convertConfig = mapAttrs' ( + name: value: + let + # Convert foo' into 'foo+' for support for '+=' syntax. + newName = if hasSuffix "'" name then substring 0 (stringLength name - 1) name + "+" else name; + + # Get the stringified value. + newValue = + if headerKvPairs.check value then + flatten ( + mapAttrsToList (header: values: (map (value: convertKvWithHeader header value) values)) value + ) + else if headerKvPair.check value then + mapAttrsToList convertKvWithHeader value + else if kvPairs.check value then + map convertKv value + else if kvPair.check value then + convertKv value + else if listOfAtom.check value then + mkAtomOrList value + else if lists.check value then + map mkAtomOrList value + else if atom.check value then + mkAtom value + else + invalid value; + in + nameValuePair newName newValue + ); + + mkKismetConf = + options: + (toKeyValue { listsAsDuplicateKeys = true; }) ( + filterAttrs (_: value: value != null) (convertConfig options) + ); + + cfg = config.services.kismet; +in +{ + options.services.kismet = { + enable = mkEnableOption "kismet"; + package = mkPackageOption pkgs "kismet" { }; + user = mkOption { + description = "The user to run Kismet as."; + type = types.str; + default = "kismet"; + }; + group = mkOption { + description = "The group to run Kismet as."; + type = types.str; + default = "kismet"; + }; + serverName = mkOption { + description = "The name of the server."; + type = types.str; + default = "Kismet"; + }; + serverDescription = mkOption { + description = "The description of the server."; + type = types.str; + default = "NixOS Kismet server"; + }; + logTypes = mkOption { + description = "The log types."; + type = with types; listOf str; + default = [ "kismet" ]; + }; + dataDir = mkOption { + description = "The Kismet data directory."; + type = types.path; + default = "/var/lib/kismet"; + }; + httpd = { + enable = mkOption { + description = "True to enable the HTTP server."; + type = types.bool; + default = false; + }; + address = mkOption { + description = "The address to listen on. Note that this cannot be a hostname or Kismet will not start."; + type = types.str; + default = "127.0.0.1"; + }; + port = mkOption { + description = "The port to listen on."; + type = types.port; + default = 2501; + }; + }; + settings = mkOption { + description = '' + Options for Kismet. See: + https://www.kismetwireless.net/docs/readme/configuring/configfiles/ + ''; + default = { }; + type = with types; attrsOf topLevel; + example = literalExpression '' + { + /* Examples for atoms */ + # dot11_link_bssts=false + dot11_link_bssts = false; # Boolean + + # dot11_related_bss_window=10000000 + dot11_related_bss_window = 10000000; # Integer + + # devicefound=00:11:22:33:44:55 + devicefound = "00:11:22:33:44:55"; # String + + # log_types+=wiglecsv + log_types' = "wiglecsv"; + + /* Examples for lists of atoms */ + # wepkey=00:DE:AD:C0:DE:00,FEEDFACE42 + wepkey = [ "00:DE:AD:C0:DE:00" "FEEDFACE42" ]; + + # alert=ADHOCCONFLICT,5/min,1/sec + # alert=ADVCRYPTCHANGE,5/min,1/sec + alert = [ + [ "ADHOCCONFLICT" "5/min" "1/sec" ] + [ "ADVCRYPTCHANGE" "5/min" "1/sec" ] + ]; + + /* Examples for sets of atoms */ + # source=wlan0:name=ath11k + source.wlan0 = { name = "ath11k"; }; + + /* Examples with colon-suffixed headers */ + # gps=gpsd:host=localhost,port=2947 + gps.gpsd = { + host = "localhost"; + port = 2947; + }; + + # apspoof=Foo1:ssid=Bar1,validmacs="00:11:22:33:44:55,aa:bb:cc:dd:ee:ff" + # apspoof=Foo1:ssid=Bar2,validmacs="01:12:23:34:45:56,ab:bc:cd:de:ef:f0" + # apspoof=Foo2:ssid=Baz1,validmacs="11:22:33:44:55:66,bb:cc:dd:ee:ff:00" + apspoof.Foo1 = [ + { ssid = "Bar1"; validmacs = [ "00:11:22:33:44:55" "aa:bb:cc:dd:ee:ff" ]; } + { ssid = "Bar2"; validmacs = [ "01:12:23:34:45:56" "ab:bc:cd:de:ef:f0" ]; } + ]; + + # because Foo1 is a list, Foo2 needs to be as well + apspoof.Foo2 = [ + { + ssid = "Bar2"; + validmacs = [ "00:11:22:33:44:55" "aa:bb:cc:dd:ee:ff" ]; + }; + ]; + } + ''; + }; + extraConfig = mkOption { + description = '' + Literal Kismet config lines appended to the site config. + Note that `services.kismet.settings` allows you to define + all options here using Nix attribute sets. + ''; + default = ""; + type = types.str; + example = '' + # Looks like the following in `services.kismet.settings`: + # wepkey = [ "00:DE:AD:C0:DE:00" "FEEDFACE42" ]; + wepkey=00:DE:AD:C0:DE:00,FEEDFACE42 + ''; + }; + }; + + config = + let + configDir = "${cfg.dataDir}/.kismet"; + settings = + cfg.settings + // { + server_name = cfg.serverName; + server_description = cfg.serverDescription; + logging_enabled = cfg.logTypes != [ ]; + log_types = cfg.logTypes; + } + // optionalAttrs cfg.httpd.enable { + httpd_bind_address = cfg.httpd.address; + httpd_port = cfg.httpd.port; + httpd_auth_file = "${configDir}/kismet_httpd.conf"; + httpd_home = "${cfg.package}/share/kismet/httpd"; + }; + in + mkIf cfg.enable { + systemd.tmpfiles.settings = { + "10-kismet" = { + ${cfg.dataDir} = { + d = { + inherit (cfg) user group; + mode = "0750"; + }; + }; + ${configDir} = { + d = { + inherit (cfg) user group; + mode = "0750"; + }; + }; + }; + }; + systemd.services.kismet = + let + kismetConf = pkgs.writeText "kismet.conf" '' + ${mkKismetConf settings} + ${cfg.extraConfig} + ''; + in + { + description = "Kismet monitoring service"; + wants = [ "basic.target" ]; + after = [ + "basic.target" + "network.target" + ]; + wantedBy = [ "multi-user.target" ]; + serviceConfig = + let + capabilities = [ + "CAP_NET_ADMIN" + "CAP_NET_RAW" + ]; + kismetPreStart = pkgs.writeShellScript "kismet-pre-start" '' + owner=${escapeShellArg "${cfg.user}:${cfg.group}"} + mkdir -p ~/.kismet + + # Ensure permissions on directories Kismet uses. + chown "$owner" ~/ ~/.kismet + cd ~/.kismet + + package=${cfg.package} + if [ -d "$package/etc" ]; then + for file in "$package/etc"/*.conf; do + # Symlink the config files if they exist or are already a link. + base="''${file##*/}" + if [ ! -f "$base" ] || [ -L "$base" ]; then + ln -sf "$file" "$base" + fi + done + fi + + for file in kismet_httpd.conf; do + # Un-symlink these files. + if [ -L "$file" ]; then + cp "$file" ".$file" + rm -f "$file" + mv ".$file" "$file" + chmod 0640 "$file" + chown "$owner" "$file" + fi + done + + # Link the site config. + ln -sf ${kismetConf} kismet_site.conf + ''; + in + { + Type = "simple"; + ExecStart = escapeShellArgs [ + "${cfg.package}/bin/kismet" + "--homedir" + cfg.dataDir + "--confdir" + configDir + "--datadir" + "${cfg.package}/share" + "--no-ncurses" + "-f" + "${configDir}/kismet.conf" + ]; + WorkingDirectory = cfg.dataDir; + ExecStartPre = "+${kismetPreStart}"; + Restart = "always"; + KillMode = "control-group"; + CapabilityBoundingSet = capabilities; + AmbientCapabilities = capabilities; + LockPersonality = true; + NoNewPrivileges = true; + PrivateDevices = false; + PrivateTmp = true; + PrivateUsers = false; + ProtectClock = true; + ProtectControlGroups = true; + ProtectHome = true; + ProtectHostname = true; + ProtectKernelLogs = true; + ProtectKernelModules = true; + ProtectKernelTunables = true; + ProtectProc = "invisible"; + ProtectSystem = "full"; + RestrictNamespaces = true; + RestrictSUIDSGID = true; + User = cfg.user; + Group = cfg.group; + UMask = "0007"; + TimeoutStopSec = 30; + }; + + # Allow it to restart if the wifi interface is not up + unitConfig.StartLimitIntervalSec = 5; + }; + users.groups.${cfg.group} = { }; + users.users.${cfg.user} = { + inherit (cfg) group; + description = "User for running Kismet"; + isSystemUser = true; + home = cfg.dataDir; + }; + }; + + meta.maintainers = with lib.maintainers; [ numinit ]; +} diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index 1f866d1f6cb5..a2924622576b 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -702,6 +702,7 @@ in keyd = handleTest ./keyd.nix { }; keymap = handleTest ./keymap.nix { }; kimai = runTest ./kimai.nix; + kismet = runTest ./kismet.nix; kmonad = runTest ./kmonad.nix; knot = runTest ./knot.nix; komga = handleTest ./komga.nix { }; diff --git a/nixos/tests/kismet.nix b/nixos/tests/kismet.nix new file mode 100644 index 000000000000..878a341cedea --- /dev/null +++ b/nixos/tests/kismet.nix @@ -0,0 +1,266 @@ +{ pkgs, lib, ... }: + +let + ssid = "Hydra SmokeNet"; + psk = "stayoffmywifi"; + wlanInterface = "wlan0"; +in +{ + name = "kismet"; + + nodes = + let + hostAddress = id: "192.168.1.${toString (id + 1)}"; + serverAddress = hostAddress 1; + in + { + airgap = + { config, ... }: + { + networking.interfaces.eth1.ipv4.addresses = lib.mkForce [ + { + address = serverAddress; + prefixLength = 24; + } + ]; + services.vwifi = { + server = { + enable = true; + ports.tcp = 8212; + ports.spy = 8213; + openFirewall = true; + }; + }; + }; + + ap = + { config, ... }: + { + networking.interfaces.eth1.ipv4.addresses = lib.mkForce [ + { + address = hostAddress 2; + prefixLength = 24; + } + ]; + services.hostapd = { + enable = true; + radios.${wlanInterface} = { + channel = 1; + networks.${wlanInterface} = { + inherit ssid; + authentication = { + mode = "wpa3-sae"; + saePasswords = [ { password = psk; } ]; + enableRecommendedPairwiseCiphers = true; + }; + }; + }; + }; + services.vwifi = { + module = { + enable = true; + macPrefix = "74:F8:F6:00:01"; + }; + client = { + enable = true; + inherit serverAddress; + }; + }; + }; + + station = + { config, ... }: + { + networking.interfaces.eth1.ipv4.addresses = lib.mkForce [ + { + address = hostAddress 3; + prefixLength = 24; + } + ]; + networking.wireless = { + # No, really, we want it enabled! + enable = lib.mkOverride 0 true; + interfaces = [ wlanInterface ]; + networks = { + ${ssid} = { + inherit psk; + authProtocols = [ "SAE" ]; + }; + }; + }; + services.vwifi = { + module = { + enable = true; + macPrefix = "74:F8:F6:00:02"; + }; + client = { + enable = true; + inherit serverAddress; + }; + }; + }; + + monitor = + { config, ... }: + { + networking.interfaces.eth1.ipv4.addresses = lib.mkForce [ + { + address = hostAddress 4; + prefixLength = 24; + } + ]; + + services.kismet = { + enable = true; + serverName = "NixOS Kismet Smoke Test"; + serverDescription = "Server testing virtual wifi devices running on Hydra"; + httpd.enable = true; + # Check that the settings all eval correctly + settings = { + # Should append to log_types + log_types' = "wiglecsv"; + + # Should all generate correctly + wepkey = [ + "00:DE:AD:C0:DE:00" + "FEEDFACE42" + ]; + alert = [ + [ + "ADHOCCONFLICT" + "5/min" + "1/sec" + ] + [ + "ADVCRYPTCHANGE" + "5/min" + "1/sec" + ] + ]; + gps.gpsd = { + host = "localhost"; + port = 2947; + }; + apspoof.Foo1 = [ + { + ssid = "Bar1"; + validmacs = [ + "00:11:22:33:44:55" + "aa:bb:cc:dd:ee:ff" + ]; + } + { + ssid = "Bar2"; + validmacs = [ + "01:12:23:34:45:56" + "ab:bc:cd:de:ef:f0" + ]; + } + ]; + apspoof.Foo2 = [ + { + ssid = "Bar2"; + validmacs = [ + "00:11:22:33:44:55" + "aa:bb:cc:dd:ee:ff" + ]; + } + ]; + + # The actual source + source.${wlanInterface} = { + name = "Virtual Wifi"; + }; + }; + extraConfig = '' + # this comment should be ignored + ''; + }; + + services.vwifi = { + module = { + enable = true; + macPrefix = "74:F8:F6:00:03"; + }; + client = { + enable = true; + spy = true; + inherit serverAddress; + }; + }; + + environment.systemPackages = with pkgs; [ + config.services.kismet.package + config.services.vwifi.package + jq + ]; + }; + }; + + testScript = + { nodes, ... }: + '' + import shlex + + # Wait for the vwifi server to come up + airgap.start() + airgap.wait_for_unit("vwifi-server.service") + airgap.wait_for_open_port(${toString nodes.airgap.services.vwifi.server.ports.tcp}) + + httpd_port = ${toString nodes.monitor.services.kismet.httpd.port} + server_name = "${nodes.monitor.services.kismet.serverName}" + server_description = "${nodes.monitor.services.kismet.serverDescription}" + wlan_interface = "${wlanInterface}" + ap_essid = "${ssid}" + ap_mac_prefix = "${nodes.ap.services.vwifi.module.macPrefix}" + station_mac_prefix = "${nodes.station.services.vwifi.module.macPrefix}" + + # Spawn the other nodes. + monitor.start() + + # Wait for the monitor to come up + monitor.wait_for_unit("kismet.service") + monitor.wait_for_open_port(httpd_port) + + # Should be up but require authentication. + url = f"http://localhost:{httpd_port}" + monitor.succeed(f"curl {url} | tee /dev/stderr | grep 'Kismet'") + + # Have to set the password now. + monitor.succeed("echo httpd_username=nixos >> ~kismet/.kismet/kismet_httpd.conf") + monitor.succeed("echo httpd_password=hydra >> ~kismet/.kismet/kismet_httpd.conf") + monitor.systemctl("restart kismet.service") + monitor.wait_for_unit("kismet.service") + monitor.wait_for_open_port(httpd_port) + + # Authentication should now work. + url = f"http://nixos:hydra@localhost:{httpd_port}" + monitor.succeed(f"curl {url}/system/status.json | tee /dev/stderr | jq -e --arg serverName {shlex.quote(server_name)} --arg serverDescription {shlex.quote(server_description)} '.\"kismet.system.server_name\" == $serverName and .\"kismet.system.server_description\" == $serverDescription'") + + # Wait for the station to connect to the AP while Kismet is monitoring + ap.start() + station.start() + + unit = f"wpa_supplicant-{wlan_interface}" + + # Generate handshakes until we detect both devices + success = False + for i in range(100): + station.wait_for_unit(f"wpa_supplicant-{wlan_interface}.service") + station.succeed(f"ifconfig {wlan_interface} down && ifconfig {wlan_interface} up") + station.wait_until_succeeds(f"journalctl -u {shlex.quote(unit)} -e | grep -Eqi {shlex.quote(wlan_interface + ': CTRL-EVENT-CONNECTED - Connection to ' + ap_mac_prefix + '[0-9a-f:]* completed')}") + station.succeed(f"journalctl --rotate --unit={shlex.quote(unit)}") + station.succeed(f"sleep 3 && journalctl --vacuum-time=1s --unit={shlex.quote(unit)}") + + # We're connected, make sure Kismet sees both of our devices + status, stdout = monitor.execute(f"curl {url}/devices/views/all/last-time/0/devices.json | tee /dev/stderr | jq -e --arg macPrefix {shlex.quote(ap_mac_prefix)} --arg ssid {shlex.quote(ap_essid)} '. | (map(select((.\"kismet.device.base.macaddr\"? | startswith($macPrefix)) and .\"dot11.device\"?.\"dot11.device.last_beaconed_ssid_record\"?.\"dot11.advertisedssid.ssid\" == $ssid)) | length) == 1'") + if status != 0: + continue + status, stdout = monitor.execute(f"curl {url}/devices/views/all/last-time/0/devices.json | tee /dev/stderr | jq -e --arg macPrefix {shlex.quote(station_mac_prefix)} '. | (map(select((.\"kismet.device.base.macaddr\"? | startswith($macPrefix)))) | length) == 1'") + if status == 0: + success = True + break + + assert success + ''; +} From d9d186e18dc4d53996a05813ec3e78c3f7c67210 Mon Sep 17 00:00:00 2001 From: Morgan Jones Date: Sun, 9 Feb 2025 21:51:48 -0800 Subject: [PATCH 3/5] kismet: add nixosTests passthru --- pkgs/by-name/ki/kismet/package.nix | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pkgs/by-name/ki/kismet/package.nix b/pkgs/by-name/ki/kismet/package.nix index 9bb47dc7c7ca..cc38baff02f9 100644 --- a/pkgs/by-name/ki/kismet/package.nix +++ b/pkgs/by-name/ki/kismet/package.nix @@ -15,6 +15,7 @@ lm_sensors, networkmanager, nix-update-script, + nixosTests, pcre2, pkg-config, openssl, @@ -115,6 +116,7 @@ stdenv.mkDerivation (finalPackage: { enableParallelBuilding = true; passthru = { + tests.kismet = nixosTests.kismet; updateScript = nix-update-script { extraArgs = [ "--version-regex" From a4371c50b3ab5b75605363546e8093d5fb6f24d2 Mon Sep 17 00:00:00 2001 From: Morgan Jones Date: Sun, 16 Feb 2025 02:02:33 -0800 Subject: [PATCH 4/5] release-notes: add kismet and vwifi --- nixos/doc/manual/release-notes/rl-2505.section.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/nixos/doc/manual/release-notes/rl-2505.section.md b/nixos/doc/manual/release-notes/rl-2505.section.md index 637d578088b9..27e561ccc6cc 100644 --- a/nixos/doc/manual/release-notes/rl-2505.section.md +++ b/nixos/doc/manual/release-notes/rl-2505.section.md @@ -57,6 +57,10 @@ - [Kimai](https://www.kimai.org/), a web-based multi-user time-tracking application. Available as [services.kimai](options.html#opt-services.kimai). +- [Kismet](https://www.kismetwireless.net/), a Wi-Fi, Bluetooth, and RF monitoring application supporting a wide range of hardware. Available as {option}`services.kismet`. + +- [vwifi](https://github.com/Raizo62/vwifi), a Wi-Fi simulator daemon leveraging the `mac80211_hwsim` and `vhost_vsock` kernel modules for efficient simulation of multi-node Wi-Fi networks. Available as {option}`services.vwifi`. + - [Homer](https://homer-demo.netlify.app/), a very simple static homepage for your server. Available as [services.homer](options.html#opt-services.homer). - [Ghidra](https://ghidra-sre.org/), a software reverse engineering (SRE) suite of tools. Available as [programs.ghidra](options.html#opt-programs.ghidra). @@ -521,6 +525,8 @@ - [`services.mongodb.enableAuth`](#opt-services.mongodb.enableAuth) now uses the newer [mongosh](https://github.com/mongodb-js/mongosh) shell instead of the legacy shell to configure the initial superuser. You can configure the mongosh package to use through the [`services.mongodb.mongoshPackage`](#opt-services.mongodb.mongoshPackage) option. +- There is a new set of NixOS test tools for testing virtual Wi-Fi networks in many different topologies. See the {option}`services.vwifi` module, {option}`services.kismet` NixOS test, and [manual](https://nixos.org/manual/nixpkgs/unstable/#sec-nixos-test-wifi) for documentation and examples. + - The paperless module now has an option for regular automatic export of documents data using the integrated document exporter. From 85fef808ad9ad2eda54b8bbe7590b4cafc1a95e2 Mon Sep 17 00:00:00 2001 From: Morgan Jones Date: Sun, 16 Feb 2025 02:24:35 -0800 Subject: [PATCH 5/5] doc: init Testing Hardware Features section with vwifi --- .../manual/development/nixos-tests.chapter.md | 1 + .../testing-hardware-features.section.md | 152 ++++++++++++++++++ nixos/doc/manual/redirects.json | 18 +++ 3 files changed, 171 insertions(+) create mode 100644 nixos/doc/manual/development/testing-hardware-features.section.md diff --git a/nixos/doc/manual/development/nixos-tests.chapter.md b/nixos/doc/manual/development/nixos-tests.chapter.md index ec0e4b9f076a..889a90bae68b 100644 --- a/nixos/doc/manual/development/nixos-tests.chapter.md +++ b/nixos/doc/manual/development/nixos-tests.chapter.md @@ -10,4 +10,5 @@ writing-nixos-tests.section.md running-nixos-tests.section.md running-nixos-tests-interactively.section.md linking-nixos-tests-to-packages.section.md +testing-hardware-features.section.md ``` diff --git a/nixos/doc/manual/development/testing-hardware-features.section.md b/nixos/doc/manual/development/testing-hardware-features.section.md new file mode 100644 index 000000000000..aaf652d731f7 --- /dev/null +++ b/nixos/doc/manual/development/testing-hardware-features.section.md @@ -0,0 +1,152 @@ +# Testing Hardware Features {#sec-nixos-test-testing-hardware-features} + +This section covers how to test various features using NixOS tests that would +normally only be possible with hardware. It is designed to showcase the NixOS test +framework's flexibility when combined with various hardware simulation libraries +or kernel modules. + +## Wi-Fi {#sec-nixos-test-wifi} + +Use `services.vwifi` to set up a virtual Wi-Fi physical layer. Create at least two nodes +for this kind of test: one with vwifi active, and either a station or an access point. +Give each a static IP address on the test network so they will never collide. +This module likely supports other topologies too; document them if you make one. + +This NixOS module leverages [vwifi](https://github.com/Raizo62/vwifi). Read the +upstream repository's documentation for more information. + +### vwifi server {#sec-nixos-test-wifi-vwifi-server} + +This node runs the vwifi server, and otherwise does not interact with the network. +You can run `vwifi-ctrl` on this node to control characteristics of the simulated +physical layer. + +```nix +airgap = + { config, ... }: + { + networking.interfaces.eth1.ipv4.addresses = lib.mkForce [ + { + address = "192.168.1.2"; + prefixLength = 24; + } + ]; + services.vwifi = { + server = { + enable = true; + ports.tcp = 8212; + # uncomment if you want to enable monitor mode on another node + # ports.spy = 8213; + openFirewall = true; + }; + }; + }; +``` + +### AP {#sec-nixos-test-wifi-ap} + +A node like this will act as a wireless access point in infrastructure mode. + +```nix +ap = + { config, ... }: + { + networking.interfaces.eth1.ipv4.addresses = lib.mkForce [ + { + address = "192.168.1.3"; + prefixLength = 24; + } + ]; + services.hostapd = { + enable = true; + radios.wlan0 = { + channel = 1; + networks.wlan0 = { + ssid = "NixOS Test Wi-Fi Network"; + authentication = { + mode = "wpa3-sae"; + saePasswords = [ { password = "supersecret"; } ]; + enableRecommendedPairwiseCiphers = true; + }; + }; + }; + }; + services.vwifi = { + module = { + enable = true; + macPrefix = "74:F8:F6:00:01"; + }; + client = { + enable = true; + serverAddress = "192.168.1.2"; + }; + }; + }; +``` + +### Station {#sec-nixos-test-wifi-station} + +A node like this acts as a wireless client. + +```nix +station = + { config, ... }: + { + networking.interfaces.eth1.ipv4.addresses = lib.mkForce [ + { + address = "192.168.1.3"; + prefixLength = 24; + } + ]; + networking.wireless = { + # No, really, we want it enabled! + enable = lib.mkOverride 0 true; + interfaces = [ "wlan0" ]; + networks = { + "NixOS Test Wi-Fi Network" = { + psk = "supersecret"; + authProtocols = [ "SAE" ]; + }; + }; + }; + services.vwifi = { + module = { + enable = true; + macPrefix = "74:F8:F6:00:02"; + }; + client = { + enable = true; + serverAddress = "192.168.1.2"; + }; + }; + }; +``` + +### Monitor {#sec-nixos-test-wifi-monitor} + +When the monitor mode interface is enabled, this node will receive +all packets broadcast by all other nodes through the spy interface. + +```nix +monitor = + { config, ... }: + { + networking.interfaces.eth1.ipv4.addresses = lib.mkForce [ + { + address = "192.168.1.4"; + prefixLength = 24; + } + ]; + + services.vwifi = { + module = { + enable = true; + macPrefix = "74:F8:F6:00:03"; + }; + client = { + enable = true; + spy = true; + serverAddress = "192.168.1.2"; + }; + }; +``` diff --git a/nixos/doc/manual/redirects.json b/nixos/doc/manual/redirects.json index 313610aa0b07..ea98245ed2ae 100644 --- a/nixos/doc/manual/redirects.json +++ b/nixos/doc/manual/redirects.json @@ -77,6 +77,21 @@ "sec-mattermost-plugins-build": [ "index.html#sec-mattermost-plugins-build" ], + "sec-nixos-test-wifi": [ + "index.html#sec-nixos-test-wifi" + ], + "sec-nixos-test-wifi-ap": [ + "index.html#sec-nixos-test-wifi-ap" + ], + "sec-nixos-test-wifi-monitor": [ + "index.html#sec-nixos-test-wifi-monitor" + ], + "sec-nixos-test-wifi-station": [ + "index.html#sec-nixos-test-wifi-station" + ], + "sec-nixos-test-wifi-vwifi-server": [ + "index.html#sec-nixos-test-wifi-vwifi-server" + ], "sec-obtaining": [ "index.html#sec-obtaining" ], @@ -1895,6 +1910,9 @@ "sec-linking-nixos-tests-to-packages": [ "index.html#sec-linking-nixos-tests-to-packages" ], + "sec-nixos-test-testing-hardware-features": [ + "index.html#sec-nixos-test-testing-hardware-features" + ], "chap-developing-the-test-driver": [ "index.html#chap-developing-the-test-driver" ],