nixos/{vwifi,kismet}: init modules (#380819)
This commit is contained in:
@@ -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";
|
||||
};
|
||||
};
|
||||
```
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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 ];
|
||||
}
|
||||
@@ -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 ];
|
||||
}
|
||||
@@ -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 { };
|
||||
|
||||
@@ -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
|
||||
'';
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user