1{ pkgs, lib, ... }:
2
3let
4 ssid = "Hydra SmokeNet";
5 psk = "stayoffmywifi";
6 wlanInterface = "wlan0";
7in
8{
9 name = "kismet";
10
11 nodes =
12 let
13 hostAddress = id: "192.168.1.${toString (id + 1)}";
14 serverAddress = hostAddress 1;
15 in
16 {
17 airgap =
18 { config, ... }:
19 {
20 networking.interfaces.eth1.ipv4.addresses = lib.mkForce [
21 {
22 address = serverAddress;
23 prefixLength = 24;
24 }
25 ];
26 services.vwifi = {
27 server = {
28 enable = true;
29 ports.tcp = 8212;
30 ports.spy = 8213;
31 openFirewall = true;
32 };
33 };
34 };
35
36 ap =
37 { config, ... }:
38 {
39 networking.interfaces.eth1.ipv4.addresses = lib.mkForce [
40 {
41 address = hostAddress 2;
42 prefixLength = 24;
43 }
44 ];
45 services.hostapd = {
46 enable = true;
47 radios.${wlanInterface} = {
48 channel = 1;
49 networks.${wlanInterface} = {
50 inherit ssid;
51 authentication = {
52 mode = "wpa3-sae";
53 saePasswords = [ { password = psk; } ];
54 enableRecommendedPairwiseCiphers = true;
55 };
56 };
57 };
58 };
59 services.vwifi = {
60 module = {
61 enable = true;
62 macPrefix = "74:F8:F6:00:01";
63 };
64 client = {
65 enable = true;
66 inherit serverAddress;
67 };
68 };
69 };
70
71 station =
72 { config, ... }:
73 {
74 networking.interfaces.eth1.ipv4.addresses = lib.mkForce [
75 {
76 address = hostAddress 3;
77 prefixLength = 24;
78 }
79 ];
80 networking.wireless = {
81 # No, really, we want it enabled!
82 enable = lib.mkOverride 0 true;
83 interfaces = [ wlanInterface ];
84 networks = {
85 ${ssid} = {
86 inherit psk;
87 authProtocols = [ "SAE" ];
88 };
89 };
90 };
91 services.vwifi = {
92 module = {
93 enable = true;
94 macPrefix = "74:F8:F6:00:02";
95 };
96 client = {
97 enable = true;
98 inherit serverAddress;
99 };
100 };
101 };
102
103 monitor =
104 { config, ... }:
105 {
106 networking.interfaces.eth1.ipv4.addresses = lib.mkForce [
107 {
108 address = hostAddress 4;
109 prefixLength = 24;
110 }
111 ];
112
113 services.kismet = {
114 enable = true;
115 serverName = "NixOS Kismet Smoke Test";
116 serverDescription = "Server testing virtual wifi devices running on Hydra";
117 httpd.enable = true;
118 # Check that the settings all eval correctly
119 settings = {
120 # Should append to log_types
121 log_types' = "wiglecsv";
122
123 # Should all generate correctly
124 wepkey = [
125 "00:DE:AD:C0:DE:00"
126 "FEEDFACE42"
127 ];
128 alert = [
129 [
130 "ADHOCCONFLICT"
131 "5/min"
132 "1/sec"
133 ]
134 [
135 "ADVCRYPTCHANGE"
136 "5/min"
137 "1/sec"
138 ]
139 ];
140 gps.gpsd = {
141 host = "localhost";
142 port = 2947;
143 };
144 apspoof.Foo1 = [
145 {
146 ssid = "Bar1";
147 validmacs = [
148 "00:11:22:33:44:55"
149 "aa:bb:cc:dd:ee:ff"
150 ];
151 }
152 {
153 ssid = "Bar2";
154 validmacs = [
155 "01:12:23:34:45:56"
156 "ab:bc:cd:de:ef:f0"
157 ];
158 }
159 ];
160 apspoof.Foo2 = [
161 {
162 ssid = "Bar2";
163 validmacs = [
164 "00:11:22:33:44:55"
165 "aa:bb:cc:dd:ee:ff"
166 ];
167 }
168 ];
169
170 # The actual source
171 source.${wlanInterface} = {
172 name = "Virtual Wifi";
173 };
174 };
175 extraConfig = ''
176 # this comment should be ignored
177 '';
178 };
179
180 services.vwifi = {
181 module = {
182 enable = true;
183 macPrefix = "74:F8:F6:00:03";
184 };
185 client = {
186 enable = true;
187 spy = true;
188 inherit serverAddress;
189 };
190 };
191
192 environment.systemPackages = with pkgs; [
193 config.services.kismet.package
194 config.services.vwifi.package
195 jq
196 ];
197 };
198 };
199
200 testScript =
201 { nodes, ... }:
202 ''
203 import shlex
204
205 # Wait for the vwifi server to come up
206 airgap.start()
207 airgap.wait_for_unit("vwifi-server.service")
208 airgap.wait_for_open_port(${toString nodes.airgap.services.vwifi.server.ports.tcp})
209
210 httpd_port = ${toString nodes.monitor.services.kismet.httpd.port}
211 server_name = "${nodes.monitor.services.kismet.serverName}"
212 server_description = "${nodes.monitor.services.kismet.serverDescription}"
213 wlan_interface = "${wlanInterface}"
214 ap_essid = "${ssid}"
215 ap_mac_prefix = "${nodes.ap.services.vwifi.module.macPrefix}"
216 station_mac_prefix = "${nodes.station.services.vwifi.module.macPrefix}"
217
218 # Spawn the other nodes.
219 monitor.start()
220
221 # Wait for the monitor to come up
222 monitor.wait_for_unit("kismet.service")
223 monitor.wait_for_open_port(httpd_port)
224
225 # Should be up but require authentication.
226 url = f"http://localhost:{httpd_port}"
227 monitor.succeed(f"curl {url} | tee /dev/stderr | grep '<title>Kismet</title>'")
228
229 # Have to set the password now.
230 monitor.succeed("echo httpd_username=nixos >> ~kismet/.kismet/kismet_httpd.conf")
231 monitor.succeed("echo httpd_password=hydra >> ~kismet/.kismet/kismet_httpd.conf")
232 monitor.systemctl("restart kismet.service")
233 monitor.wait_for_unit("kismet.service")
234 monitor.wait_for_open_port(httpd_port)
235
236 # Authentication should now work.
237 url = f"http://nixos:hydra@localhost:{httpd_port}"
238 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'")
239
240 # Wait for the station to connect to the AP while Kismet is monitoring
241 ap.start()
242 station.start()
243
244 unit = f"wpa_supplicant-{wlan_interface}"
245
246 # Generate handshakes until we detect both devices
247 success = False
248 for i in range(100):
249 station.wait_for_unit(f"wpa_supplicant-{wlan_interface}.service")
250 station.succeed(f"ifconfig {wlan_interface} down && ifconfig {wlan_interface} up")
251 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')}")
252 station.succeed(f"journalctl --rotate --unit={shlex.quote(unit)}")
253 station.succeed(f"sleep 3 && journalctl --vacuum-time=1s --unit={shlex.quote(unit)}")
254
255 # We're connected, make sure Kismet sees both of our devices
256 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'")
257 if status != 0:
258 continue
259 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'")
260 if status == 0:
261 success = True
262 break
263
264 assert success
265 '';
266}