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

Changed files
+1107
nixos
pkgs
by-name
ki
kismet
+1
nixos/doc/manual/development/nixos-tests.chapter.md
···
running-nixos-tests.section.md
running-nixos-tests-interactively.section.md
linking-nixos-tests-to-packages.section.md
+
testing-hardware-features.section.md
```
+152
nixos/doc/manual/development/testing-hardware-features.section.md
···
+
# 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
nixos/doc/manual/redirects.json
···
"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"
],
···
],
"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"
+6
nixos/doc/manual/release-notes/rl-2505.section.md
···
- [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).
···
- `bind.cacheNetworks` now only controls access for recursive queries, where it previously controlled access for all queries.
- [`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
nixos/modules/module-list.nix
···
./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
···
./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
+459
nixos/modules/services/networking/kismet.nix
···
+
{
+
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
nixos/modules/services/networking/vwifi.nix
···
+
{
+
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
nixos/tests/all-tests.nix
···
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
nixos/tests/kismet.nix
···
+
{ 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
pkgs/by-name/ki/kismet/package.nix
···
lm_sensors,
networkmanager,
nix-update-script,
+
nixosTests,
pcre2,
pkg-config,
openssl,
···
enableParallelBuilding = true;
passthru = {
+
tests.kismet = nixosTests.kismet;
updateScript = nix-update-script {
extraArgs = [
"--version-regex"