]>
Commit | Line | Data |
---|---|---|
b8cd4c54 | 1 | # Python class for controlling hostapd |
36408936 | 2 | # Copyright (c) 2013-2014, Jouni Malinen <j@w1.fi> |
b8cd4c54 JM |
3 | # |
4 | # This software may be distributed under the terms of the BSD license. | |
5 | # See README for more details. | |
6 | ||
7 | import os | |
8 | import time | |
9 | import logging | |
bfe375ec JM |
10 | import binascii |
11 | import struct | |
b8cd4c54 | 12 | import wpaspy |
8ce4855b | 13 | import remotehost |
b8cd4c54 | 14 | |
c9aa4308 | 15 | logger = logging.getLogger() |
b8cd4c54 | 16 | hapd_ctrl = '/var/run/hostapd' |
8c87f65f | 17 | hapd_global = '/var/run/hostapd-global' |
b8cd4c54 | 18 | |
bfe375ec JM |
19 | def mac2tuple(mac): |
20 | return struct.unpack('6B', binascii.unhexlify(mac.replace(':',''))) | |
21 | ||
b8cd4c54 | 22 | class HostapdGlobal: |
cb73f7e8 | 23 | def __init__(self, hostname=None, port=8878): |
8ce4855b | 24 | self.host = remotehost.Host(hostname) |
cb73f7e8 JD |
25 | self.hostname = hostname |
26 | self.port = port | |
27 | if hostname is None: | |
28 | self.ctrl = wpaspy.Ctrl(hapd_global) | |
29 | self.mon = wpaspy.Ctrl(hapd_global) | |
d4944fad | 30 | self.dbg = "" |
cb73f7e8 JD |
31 | else: |
32 | self.ctrl = wpaspy.Ctrl(hostname, port) | |
33 | self.mon = wpaspy.Ctrl(hostname, port) | |
d4944fad | 34 | self.dbg = hostname + "/" + str(port) |
41a256ec AN |
35 | self.mon.attach() |
36 | ||
d4944fad JD |
37 | def request(self, cmd, timeout=10): |
38 | logger.debug(self.dbg + ": CTRL(global): " + cmd) | |
39 | return self.ctrl.request(cmd, timeout) | |
41a256ec AN |
40 | |
41 | def wait_event(self, events, timeout): | |
42 | start = os.times()[4] | |
43 | while True: | |
44 | while self.mon.pending(): | |
45 | ev = self.mon.recv() | |
d4944fad | 46 | logger.debug(self.dbg + "(global): " + ev) |
41a256ec AN |
47 | for event in events: |
48 | if event in ev: | |
49 | return ev | |
50 | now = os.times()[4] | |
51 | remaining = start + timeout - now | |
52 | if remaining <= 0: | |
53 | break | |
54 | if not self.mon.pending(timeout=remaining): | |
55 | break | |
56 | return None | |
b8cd4c54 | 57 | |
08763216 JM |
58 | def add(self, ifname, driver=None): |
59 | cmd = "ADD " + ifname + " " + hapd_ctrl | |
60 | if driver: | |
61 | cmd += " " + driver | |
d4944fad | 62 | res = self.request(cmd) |
b8cd4c54 JM |
63 | if not "OK" in res: |
64 | raise Exception("Could not add hostapd interface " + ifname) | |
65 | ||
77990cd7 | 66 | def add_iface(self, ifname, confname): |
d4944fad | 67 | res = self.request("ADD " + ifname + " config=" + confname) |
77990cd7 JM |
68 | if not "OK" in res: |
69 | raise Exception("Could not add hostapd interface") | |
70 | ||
a6333977 | 71 | def add_bss(self, phy, confname, ignore_error=False): |
d4944fad | 72 | res = self.request("ADD bss_config=" + phy + ":" + confname) |
a6333977 JM |
73 | if not "OK" in res: |
74 | if not ignore_error: | |
75 | raise Exception("Could not add hostapd BSS") | |
76 | ||
b8cd4c54 | 77 | def remove(self, ifname): |
d4944fad | 78 | self.request("REMOVE " + ifname, timeout=30) |
b8cd4c54 | 79 | |
75428961 | 80 | def relog(self): |
d4944fad | 81 | self.request("RELOG") |
75428961 | 82 | |
f8949f5f | 83 | def flush(self): |
d4944fad | 84 | self.request("FLUSH") |
f8949f5f | 85 | |
4d48d44c JD |
86 | def get_ctrl_iface_port(self, ifname): |
87 | if self.hostname is None: | |
88 | return None | |
89 | ||
d4944fad | 90 | res = self.request("INTERFACES ctrl") |
4d48d44c JD |
91 | lines = res.splitlines() |
92 | found = False | |
93 | for line in lines: | |
94 | words = line.split() | |
95 | if words[0] == ifname: | |
96 | found = True | |
97 | break | |
98 | if not found: | |
99 | raise Exception("Could not find UDP port for " + ifname) | |
100 | res = line.find("ctrl_iface=udp:") | |
101 | if res == -1: | |
102 | raise Exception("Wrong ctrl_interface format") | |
103 | words = line.split(":") | |
104 | return int(words[1]) | |
b8cd4c54 | 105 | |
e3b36d42 JD |
106 | def terminate(self): |
107 | self.mon.detach() | |
108 | self.mon.close() | |
109 | self.mon = None | |
110 | self.ctrl.terminate() | |
111 | self.ctrl = None | |
112 | ||
b8cd4c54 | 113 | class Hostapd: |
cb73f7e8 | 114 | def __init__(self, ifname, bssidx=0, hostname=None, port=8877): |
8ce4855b | 115 | self.host = remotehost.Host(hostname, ifname) |
b8cd4c54 | 116 | self.ifname = ifname |
cb73f7e8 JD |
117 | if hostname is None: |
118 | self.ctrl = wpaspy.Ctrl(os.path.join(hapd_ctrl, ifname)) | |
119 | self.mon = wpaspy.Ctrl(os.path.join(hapd_ctrl, ifname)) | |
d4944fad | 120 | self.dbg = ifname |
cb73f7e8 JD |
121 | else: |
122 | self.ctrl = wpaspy.Ctrl(hostname, port) | |
123 | self.mon = wpaspy.Ctrl(hostname, port) | |
d4944fad | 124 | self.dbg = hostname + "/" + ifname |
b47750be | 125 | self.mon.attach() |
f6420942 | 126 | self.bssid = None |
54cf411f | 127 | self.bssidx = bssidx |
f6420942 | 128 | |
e3b36d42 JD |
129 | def close_ctrl(self): |
130 | if self.mon is not None: | |
131 | self.mon.detach() | |
132 | self.mon.close() | |
133 | self.mon = None | |
134 | self.ctrl.close() | |
135 | self.ctrl = None | |
136 | ||
f6420942 JM |
137 | def own_addr(self): |
138 | if self.bssid is None: | |
54cf411f | 139 | self.bssid = self.get_status_field('bssid[%d]' % self.bssidx) |
f6420942 | 140 | return self.bssid |
b8cd4c54 JM |
141 | |
142 | def request(self, cmd): | |
d4944fad | 143 | logger.debug(self.dbg + ": CTRL: " + cmd) |
b8cd4c54 JM |
144 | return self.ctrl.request(cmd) |
145 | ||
146 | def ping(self): | |
147 | return "PONG" in self.request("PING") | |
148 | ||
149 | def set(self, field, value): | |
b8cd4c54 JM |
150 | if not "OK" in self.request("SET " + field + " " + value): |
151 | raise Exception("Failed to set hostapd parameter " + field) | |
152 | ||
153 | def set_defaults(self): | |
154 | self.set("driver", "nl80211") | |
155 | self.set("hw_mode", "g") | |
156 | self.set("channel", "1") | |
157 | self.set("ieee80211n", "1") | |
789b9f1d JM |
158 | self.set("logger_stdout", "-1") |
159 | self.set("logger_stdout_level", "0") | |
b8cd4c54 JM |
160 | |
161 | def set_open(self, ssid): | |
162 | self.set_defaults() | |
163 | self.set("ssid", ssid) | |
164 | ||
165 | def set_wpa2_psk(self, ssid, passphrase): | |
166 | self.set_defaults() | |
167 | self.set("ssid", ssid) | |
168 | self.set("wpa_passphrase", passphrase) | |
169 | self.set("wpa", "2") | |
170 | self.set("wpa_key_mgmt", "WPA-PSK") | |
171 | self.set("rsn_pairwise", "CCMP") | |
172 | ||
e492837b JM |
173 | def set_wpa_psk(self, ssid, passphrase): |
174 | self.set_defaults() | |
175 | self.set("ssid", ssid) | |
176 | self.set("wpa_passphrase", passphrase) | |
177 | self.set("wpa", "1") | |
178 | self.set("wpa_key_mgmt", "WPA-PSK") | |
179 | self.set("wpa_pairwise", "TKIP") | |
180 | ||
181 | def set_wpa_psk_mixed(self, ssid, passphrase): | |
182 | self.set_defaults() | |
183 | self.set("ssid", ssid) | |
184 | self.set("wpa_passphrase", passphrase) | |
185 | self.set("wpa", "3") | |
186 | self.set("wpa_key_mgmt", "WPA-PSK") | |
187 | self.set("wpa_pairwise", "TKIP") | |
188 | self.set("rsn_pairwise", "CCMP") | |
189 | ||
0165c4be JM |
190 | def set_wep(self, ssid, key): |
191 | self.set_defaults() | |
192 | self.set("ssid", ssid) | |
193 | self.set("wep_key0", key) | |
194 | ||
b8cd4c54 | 195 | def enable(self): |
d45e417f | 196 | if not "OK" in self.request("ENABLE"): |
b8cd4c54 JM |
197 | raise Exception("Failed to enable hostapd interface " + self.ifname) |
198 | ||
199 | def disable(self): | |
00f74dbd | 200 | if not "OK" in self.request("DISABLE"): |
b8cd4c54 | 201 | raise Exception("Failed to disable hostapd interface " + self.ifname) |
e259d186 | 202 | |
b47750be JM |
203 | def dump_monitor(self): |
204 | while self.mon.pending(): | |
205 | ev = self.mon.recv() | |
d4944fad | 206 | logger.debug(self.dbg + ": " + ev) |
b47750be JM |
207 | |
208 | def wait_event(self, events, timeout): | |
36408936 JM |
209 | start = os.times()[4] |
210 | while True: | |
b47750be JM |
211 | while self.mon.pending(): |
212 | ev = self.mon.recv() | |
d4944fad | 213 | logger.debug(self.dbg + ": " + ev) |
b47750be JM |
214 | for event in events: |
215 | if event in ev: | |
216 | return ev | |
36408936 JM |
217 | now = os.times()[4] |
218 | remaining = start + timeout - now | |
219 | if remaining <= 0: | |
220 | break | |
221 | if not self.mon.pending(timeout=remaining): | |
222 | break | |
b47750be JM |
223 | return None |
224 | ||
225 | def get_status(self): | |
226 | res = self.request("STATUS") | |
227 | lines = res.splitlines() | |
228 | vals = dict() | |
229 | for l in lines: | |
230 | [name,value] = l.split('=', 1) | |
231 | vals[name] = value | |
232 | return vals | |
233 | ||
234 | def get_status_field(self, field): | |
235 | vals = self.get_status() | |
236 | if field in vals: | |
237 | return vals[field] | |
238 | return None | |
239 | ||
a36158be JM |
240 | def get_driver_status(self): |
241 | res = self.request("STATUS-DRIVER") | |
242 | lines = res.splitlines() | |
243 | vals = dict() | |
244 | for l in lines: | |
245 | [name,value] = l.split('=', 1) | |
246 | vals[name] = value | |
247 | return vals | |
248 | ||
249 | def get_driver_status_field(self, field): | |
250 | vals = self.get_driver_status() | |
251 | if field in vals: | |
252 | return vals[field] | |
253 | return None | |
254 | ||
65038313 JM |
255 | def get_config(self): |
256 | res = self.request("GET_CONFIG") | |
257 | lines = res.splitlines() | |
258 | vals = dict() | |
259 | for l in lines: | |
260 | [name,value] = l.split('=', 1) | |
261 | vals[name] = value | |
262 | return vals | |
263 | ||
bfe375ec JM |
264 | def mgmt_rx(self, timeout=5): |
265 | ev = self.wait_event(["MGMT-RX"], timeout=timeout) | |
266 | if ev is None: | |
267 | return None | |
268 | msg = {} | |
269 | frame = binascii.unhexlify(ev.split(' ')[1]) | |
270 | msg['frame'] = frame | |
271 | ||
272 | hdr = struct.unpack('<HH6B6B6BH', frame[0:24]) | |
273 | msg['fc'] = hdr[0] | |
274 | msg['subtype'] = (hdr[0] >> 4) & 0xf | |
275 | hdr = hdr[1:] | |
276 | msg['duration'] = hdr[0] | |
277 | hdr = hdr[1:] | |
278 | msg['da'] = "%02x:%02x:%02x:%02x:%02x:%02x" % hdr[0:6] | |
279 | hdr = hdr[6:] | |
280 | msg['sa'] = "%02x:%02x:%02x:%02x:%02x:%02x" % hdr[0:6] | |
281 | hdr = hdr[6:] | |
282 | msg['bssid'] = "%02x:%02x:%02x:%02x:%02x:%02x" % hdr[0:6] | |
283 | hdr = hdr[6:] | |
284 | msg['seq_ctrl'] = hdr[0] | |
285 | msg['payload'] = frame[24:] | |
286 | ||
287 | return msg | |
288 | ||
289 | def mgmt_tx(self, msg): | |
290 | t = (msg['fc'], 0) + mac2tuple(msg['da']) + mac2tuple(msg['sa']) + mac2tuple(msg['bssid']) + (0,) | |
291 | hdr = struct.pack('<HH6B6B6BH', *t) | |
292 | self.request("MGMT_TX " + binascii.hexlify(hdr + msg['payload'])) | |
293 | ||
cce26eb4 JM |
294 | def get_sta(self, addr, info=None, next=False): |
295 | cmd = "STA-NEXT " if next else "STA " | |
296 | if addr is None: | |
297 | res = self.request("STA-FIRST") | |
298 | elif info: | |
299 | res = self.request(cmd + addr + " " + info) | |
5dec879d | 300 | else: |
cce26eb4 | 301 | res = self.request(cmd + addr) |
6435799b JM |
302 | lines = res.splitlines() |
303 | vals = dict() | |
304 | first = True | |
305 | for l in lines: | |
2496adf0 | 306 | if first and '=' not in l: |
6435799b JM |
307 | vals['addr'] = l |
308 | first = False | |
309 | else: | |
310 | [name,value] = l.split('=', 1) | |
311 | vals[name] = value | |
312 | return vals | |
313 | ||
4fcee244 JM |
314 | def get_mib(self, param=None): |
315 | if param: | |
316 | res = self.request("MIB " + param) | |
317 | else: | |
318 | res = self.request("MIB") | |
7fd15145 JM |
319 | lines = res.splitlines() |
320 | vals = dict() | |
321 | for l in lines: | |
4fcee244 JM |
322 | name_val = l.split('=', 1) |
323 | if len(name_val) > 1: | |
324 | vals[name_val[0]] = name_val[1] | |
7fd15145 JM |
325 | return vals |
326 | ||
865fa1e9 JM |
327 | def get_pmksa(self, addr): |
328 | res = self.request("PMKSA") | |
329 | lines = res.splitlines() | |
330 | for l in lines: | |
331 | if addr not in l: | |
332 | continue | |
333 | vals = dict() | |
334 | [index,aa,pmkid,expiration,opportunistic] = l.split(' ') | |
335 | vals['index'] = index | |
336 | vals['pmkid'] = pmkid | |
337 | vals['expiration'] = expiration | |
338 | vals['opportunistic'] = opportunistic | |
339 | return vals | |
340 | return None | |
341 | ||
78b83193 JD |
342 | def add_ap(apdev, params, wait_enabled=True, no_enable=False, timeout=30): |
343 | if isinstance(apdev, dict): | |
344 | ifname = apdev['ifname'] | |
345 | try: | |
346 | hostname = apdev['hostname'] | |
347 | port = apdev['port'] | |
348 | logger.info("Starting AP " + hostname + "/" + port + " " + ifname) | |
349 | except: | |
350 | logger.info("Starting AP " + ifname) | |
351 | hostname = None | |
352 | port = 8878 | |
353 | else: | |
354 | ifname = apdev | |
355 | logger.info("Starting AP " + ifname + " (old add_ap argument type)") | |
356 | hostname = None | |
357 | port = 8878 | |
cb73f7e8 | 358 | hapd_global = HostapdGlobal(hostname=hostname, port=port) |
e259d186 JM |
359 | hapd_global.remove(ifname) |
360 | hapd_global.add(ifname) | |
4d48d44c JD |
361 | port = hapd_global.get_ctrl_iface_port(ifname) |
362 | hapd = Hostapd(ifname, hostname=hostname, port=port) | |
e259d186 JM |
363 | if not hapd.ping(): |
364 | raise Exception("Could not ping hostapd") | |
365 | hapd.set_defaults() | |
cd7f1b9a JM |
366 | fields = [ "ssid", "wpa_passphrase", "nas_identifier", "wpa_key_mgmt", |
367 | "wpa", | |
7fd15145 | 368 | "wpa_pairwise", "rsn_pairwise", "auth_server_addr", |
97de642a | 369 | "acct_server_addr", "osu_server_uri" ] |
e259d186 JM |
370 | for field in fields: |
371 | if field in params: | |
372 | hapd.set(field, params[field]) | |
93a06242 JM |
373 | for f,v in params.items(): |
374 | if f in fields: | |
375 | continue | |
376 | if isinstance(v, list): | |
377 | for val in v: | |
378 | hapd.set(f, val) | |
379 | else: | |
380 | hapd.set(f, v) | |
138ec97e JM |
381 | if no_enable: |
382 | return hapd | |
e259d186 | 383 | hapd.enable() |
629dbdd3 | 384 | if wait_enabled: |
57ff37d0 | 385 | ev = hapd.wait_event(["AP-ENABLED", "AP-DISABLED"], timeout=timeout) |
629dbdd3 JM |
386 | if ev is None: |
387 | raise Exception("AP startup timed out") | |
f8ad9dc2 JM |
388 | if "AP-ENABLED" not in ev: |
389 | raise Exception("AP startup failed") | |
b47750be | 390 | return hapd |
e259d186 | 391 | |
cb73f7e8 JD |
392 | def add_bss(phy, ifname, confname, ignore_error=False, hostname=None, |
393 | port=8878): | |
a6333977 | 394 | logger.info("Starting BSS phy=" + phy + " ifname=" + ifname) |
cb73f7e8 | 395 | hapd_global = HostapdGlobal(hostname=hostname, port=port) |
a6333977 | 396 | hapd_global.add_bss(phy, confname, ignore_error) |
4d48d44c JD |
397 | port = hapd_global.get_ctrl_iface_port(ifname) |
398 | hapd = Hostapd(ifname, hostname=hostname, port=port) | |
a6333977 JM |
399 | if not hapd.ping(): |
400 | raise Exception("Could not ping hostapd") | |
401 | ||
cb73f7e8 | 402 | def add_iface(ifname, confname, hostname=None, port=8878): |
77990cd7 | 403 | logger.info("Starting interface " + ifname) |
cb73f7e8 | 404 | hapd_global = HostapdGlobal(hostname=hostname, port=port) |
77990cd7 | 405 | hapd_global.add_iface(ifname, confname) |
4d48d44c JD |
406 | port = hapd_global.get_ctrl_iface_port(ifname) |
407 | hapd = Hostapd(ifname, hostname=hostname, port=port) | |
77990cd7 JM |
408 | if not hapd.ping(): |
409 | raise Exception("Could not ping hostapd") | |
410 | ||
cb73f7e8 | 411 | def remove_bss(ifname, hostname=None, port=8878): |
a6333977 | 412 | logger.info("Removing BSS " + ifname) |
cb73f7e8 | 413 | hapd_global = HostapdGlobal(hostname=hostname, port=port) |
a6333977 JM |
414 | hapd_global.remove(ifname) |
415 | ||
e3b36d42 JD |
416 | def terminate(hostname=None, port=8878): |
417 | logger.info("Terminating hostapd") | |
418 | hapd_global = HostapdGlobal(hostname=hostname, port=port) | |
419 | hapd_global.terminate() | |
420 | ||
e259d186 JM |
421 | def wpa2_params(ssid=None, passphrase=None): |
422 | params = { "wpa": "2", | |
423 | "wpa_key_mgmt": "WPA-PSK", | |
424 | "rsn_pairwise": "CCMP" } | |
425 | if ssid: | |
426 | params["ssid"] = ssid | |
427 | if passphrase: | |
428 | params["wpa_passphrase"] = passphrase | |
429 | return params | |
430 | ||
431 | def wpa_params(ssid=None, passphrase=None): | |
432 | params = { "wpa": "1", | |
433 | "wpa_key_mgmt": "WPA-PSK", | |
434 | "wpa_pairwise": "TKIP" } | |
435 | if ssid: | |
436 | params["ssid"] = ssid | |
437 | if passphrase: | |
438 | params["wpa_passphrase"] = passphrase | |
439 | return params | |
440 | ||
441 | def wpa_mixed_params(ssid=None, passphrase=None): | |
442 | params = { "wpa": "3", | |
443 | "wpa_key_mgmt": "WPA-PSK", | |
444 | "wpa_pairwise": "TKIP", | |
445 | "rsn_pairwise": "CCMP" } | |
446 | if ssid: | |
447 | params["ssid"] = ssid | |
448 | if passphrase: | |
449 | params["wpa_passphrase"] = passphrase | |
450 | return params | |
9626962d JM |
451 | |
452 | def radius_params(): | |
453 | params = { "auth_server_addr": "127.0.0.1", | |
454 | "auth_server_port": "1812", | |
455 | "auth_server_shared_secret": "radius", | |
456 | "nas_identifier": "nas.w1.fi" } | |
457 | return params | |
458 | ||
71390dc8 JM |
459 | def wpa_eap_params(ssid=None): |
460 | params = radius_params() | |
461 | params["wpa"] = "1" | |
462 | params["wpa_key_mgmt"] = "WPA-EAP" | |
463 | params["wpa_pairwise"] = "TKIP" | |
464 | params["ieee8021x"] = "1" | |
465 | if ssid: | |
466 | params["ssid"] = ssid | |
467 | return params | |
468 | ||
9626962d JM |
469 | def wpa2_eap_params(ssid=None): |
470 | params = radius_params() | |
471 | params["wpa"] = "2" | |
472 | params["wpa_key_mgmt"] = "WPA-EAP" | |
473 | params["rsn_pairwise"] = "CCMP" | |
474 | params["ieee8021x"] = "1" | |
475 | if ssid: | |
476 | params["ssid"] = ssid | |
477 | return params | |
c0ca24fc JD |
478 | |
479 | def b_only_params(channel="1", ssid=None, country=None): | |
480 | params = { "hw_mode" : "b", | |
481 | "channel" : channel } | |
482 | if ssid: | |
483 | params["ssid"] = ssid | |
484 | if country: | |
485 | params["country_code"] = country | |
486 | return params | |
487 | ||
488 | def g_only_params(channel="1", ssid=None, country=None): | |
489 | params = { "hw_mode" : "g", | |
490 | "channel" : channel } | |
491 | if ssid: | |
492 | params["ssid"] = ssid | |
493 | if country: | |
494 | params["country_code"] = country | |
495 | return params | |
496 | ||
497 | def a_only_params(channel="36", ssid=None, country=None): | |
498 | params = { "hw_mode" : "a", | |
499 | "channel" : channel } | |
500 | if ssid: | |
501 | params["ssid"] = ssid | |
502 | if country: | |
503 | params["country_code"] = country | |
504 | return params | |
505 | ||
506 | def ht20_params(channel="1", ssid=None, country=None): | |
507 | params = { "ieee80211n" : "1", | |
508 | "channel" : channel, | |
509 | "hw_mode" : "g" } | |
510 | if int(channel) > 14: | |
511 | params["hw_mode"] = "a" | |
512 | if ssid: | |
513 | params["ssid"] = ssid | |
514 | if country: | |
515 | params["country_code"] = country | |
516 | return params | |
517 | ||
518 | def ht40_plus_params(channel="1", ssid=None, country=None): | |
519 | params = ht20_params(channel, ssid, country) | |
520 | params['ht_capab'] = "[HT40+]" | |
521 | return params | |
522 | ||
523 | def ht40_minus_params(channel="1", ssid=None, country=None): | |
524 | params = ht20_params(channel, ssid, country) | |
525 | params['ht_capab'] = "[HT40-]" | |
526 | return params |