nixos/{vwifi,kismet}: init modules (#380819)

This commit is contained in:
Tristan Ross
2025-04-20 17:02:03 -07:00
committed by GitHub
10 changed files with 1107 additions and 0 deletions
@@ -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
```
@@ -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";
};
};
```
+18
View File
@@ -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"
],
@@ -56,6 +56,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).
@@ -522,6 +526,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.
+2
View File
@@ -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
@@ -1352,6 +1353,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
@@ -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 ];
}
+200
View File
@@ -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 ];
}
+1
View File
@@ -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 { };
+266
View File
@@ -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 '<title>Kismet</title>'")
# 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
'';
}
+2
View File
@@ -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"