]>
Commit | Line | Data |
---|---|---|
ebc61dc2 JM |
1 | # GAS tests |
2 | # Copyright (c) 2013, Qualcomm Atheros, Inc. | |
d7a99700 | 3 | # Copyright (c) 2013-2014, Jouni Malinen <j@w1.fi> |
ebc61dc2 JM |
4 | # |
5 | # This software may be distributed under the terms of the BSD license. | |
6 | # See README for more details. | |
7 | ||
8 | import time | |
bfe375ec | 9 | import binascii |
ebc61dc2 | 10 | import logging |
c9aa4308 | 11 | logger = logging.getLogger() |
ebc61dc2 | 12 | import re |
d9474958 | 13 | import struct |
ebc61dc2 JM |
14 | |
15 | import hostapd | |
16 | ||
17 | def hs20_ap_params(): | |
18 | params = hostapd.wpa2_params(ssid="test-gas") | |
19 | params['wpa_key_mgmt'] = "WPA-EAP" | |
20 | params['ieee80211w'] = "1" | |
21 | params['ieee8021x'] = "1" | |
22 | params['auth_server_addr'] = "127.0.0.1" | |
23 | params['auth_server_port'] = "1812" | |
24 | params['auth_server_shared_secret'] = "radius" | |
25 | params['interworking'] = "1" | |
26 | params['access_network_type'] = "14" | |
27 | params['internet'] = "1" | |
28 | params['asra'] = "0" | |
29 | params['esr'] = "0" | |
30 | params['uesa'] = "0" | |
31 | params['venue_group'] = "7" | |
32 | params['venue_type'] = "1" | |
33 | params['venue_name'] = [ "eng:Example venue", "fin:Esimerkkipaikka" ] | |
34 | params['roaming_consortium'] = [ "112233", "1020304050", "010203040506", | |
35 | "fedcba" ] | |
36 | params['domain_name'] = "example.com,another.example.com" | |
37 | params['nai_realm'] = [ "0,example.com,13[5:6],21[2:4][5:7]", | |
38 | "0,another.example.com" ] | |
39 | params['anqp_3gpp_cell_net'] = "244,91" | |
cef16c47 JM |
40 | params['network_auth_type'] = "02http://www.example.com/redirect/me/here/" |
41 | params['ipaddr_type_availability'] = "14" | |
42 | params['hs20'] = "1" | |
43 | params['hs20_oper_friendly_name'] = [ "eng:Example operator", "fin:Esimerkkioperaattori" ] | |
44 | params['hs20_wan_metrics'] = "01:8000:1000:80:240:3000" | |
45 | params['hs20_conn_capab'] = [ "1:0:2", "6:22:1", "17:5060:0" ] | |
46 | params['hs20_operating_class'] = "5173" | |
ebc61dc2 JM |
47 | return params |
48 | ||
18dd2af0 JM |
49 | def start_ap(ap): |
50 | params = hs20_ap_params() | |
51 | params['hessid'] = ap['bssid'] | |
52 | hostapd.add_ap(ap['ifname'], params) | |
53 | return hostapd.Hostapd(ap['ifname']) | |
54 | ||
ebc61dc2 JM |
55 | def get_gas_response(dev, bssid, info, allow_fetch_failure=False): |
56 | exp = r'<.>(GAS-RESPONSE-INFO) addr=([0-9a-f:]*) dialog_token=([0-9]*) status_code=([0-9]*) resp_len=([\-0-9]*)' | |
57 | res = re.split(exp, info) | |
58 | if len(res) < 6: | |
59 | raise Exception("Could not parse GAS-RESPONSE-INFO") | |
60 | if res[2] != bssid: | |
61 | raise Exception("Unexpected BSSID in response") | |
62 | token = res[3] | |
63 | status = res[4] | |
64 | if status != "0": | |
65 | raise Exception("GAS query failed") | |
66 | resp_len = res[5] | |
67 | if resp_len == "-1": | |
68 | raise Exception("GAS query reported invalid response length") | |
69 | if int(resp_len) > 2000: | |
70 | raise Exception("Unexpected long GAS response") | |
71 | ||
72 | resp = dev.request("GAS_RESPONSE_GET " + bssid + " " + token) | |
73 | if "FAIL" in resp: | |
74 | if allow_fetch_failure: | |
75 | logger.debug("GAS response was not available anymore") | |
76 | return | |
77 | raise Exception("Could not fetch GAS response") | |
78 | if len(resp) != int(resp_len) * 2: | |
79 | raise Exception("Unexpected GAS response length") | |
80 | logger.debug("GAS response: " + resp) | |
81 | ||
82 | def test_gas_generic(dev, apdev): | |
83 | """Generic GAS query""" | |
84 | bssid = apdev[0]['bssid'] | |
85 | params = hs20_ap_params() | |
86 | params['hessid'] = bssid | |
87 | hostapd.add_ap(apdev[0]['ifname'], params) | |
88 | ||
adf277a0 | 89 | dev[0].scan(freq="2412") |
ebc61dc2 JM |
90 | req = dev[0].request("GAS_REQUEST " + bssid + " 00 000102000101") |
91 | if "FAIL" in req: | |
92 | raise Exception("GAS query request rejected") | |
93 | ev = dev[0].wait_event(["GAS-RESPONSE-INFO"], timeout=10) | |
94 | if ev is None: | |
95 | raise Exception("GAS query timed out") | |
96 | get_gas_response(dev[0], bssid, ev) | |
97 | ||
98 | def test_gas_concurrent_scan(dev, apdev): | |
99 | """Generic GAS queries with concurrent scan operation""" | |
100 | bssid = apdev[0]['bssid'] | |
101 | params = hs20_ap_params() | |
102 | params['hessid'] = bssid | |
103 | hostapd.add_ap(apdev[0]['ifname'], params) | |
104 | ||
d7a99700 JM |
105 | # get BSS entry available to allow GAS query |
106 | dev[0].scan(freq="2412") | |
ebc61dc2 JM |
107 | |
108 | logger.info("Request concurrent operations") | |
109 | req = dev[0].request("GAS_REQUEST " + bssid + " 00 000102000101") | |
110 | if "FAIL" in req: | |
111 | raise Exception("GAS query request rejected") | |
112 | req = dev[0].request("GAS_REQUEST " + bssid + " 00 000102000801") | |
113 | if "FAIL" in req: | |
114 | raise Exception("GAS query request rejected") | |
d7a99700 | 115 | dev[0].scan(no_wait=True) |
ebc61dc2 JM |
116 | req = dev[0].request("GAS_REQUEST " + bssid + " 00 000102000201") |
117 | if "FAIL" in req: | |
118 | raise Exception("GAS query request rejected") | |
119 | req = dev[0].request("GAS_REQUEST " + bssid + " 00 000102000501") | |
120 | if "FAIL" in req: | |
121 | raise Exception("GAS query request rejected") | |
122 | ||
123 | responses = 0 | |
124 | for i in range(0, 5): | |
125 | ev = dev[0].wait_event(["GAS-RESPONSE-INFO", "CTRL-EVENT-SCAN-RESULTS"], | |
126 | timeout=10) | |
127 | if ev is None: | |
128 | raise Exception("Operation timed out") | |
129 | if "GAS-RESPONSE-INFO" in ev: | |
130 | responses = responses + 1 | |
131 | get_gas_response(dev[0], bssid, ev, allow_fetch_failure=True) | |
132 | ||
133 | if responses != 4: | |
134 | raise Exception("Unexpected number of GAS responses") | |
135 | ||
136 | def test_gas_concurrent_connect(dev, apdev): | |
137 | """Generic GAS queries with concurrent connection operation""" | |
138 | bssid = apdev[0]['bssid'] | |
139 | params = hs20_ap_params() | |
140 | params['hessid'] = bssid | |
141 | hostapd.add_ap(apdev[0]['ifname'], params) | |
142 | ||
d7a99700 | 143 | dev[0].scan(freq="2412") |
ebc61dc2 JM |
144 | |
145 | logger.debug("Start concurrent connect and GAS request") | |
146 | dev[0].connect("test-gas", key_mgmt="WPA-EAP", eap="TTLS", | |
147 | identity="DOMAIN\mschapv2 user", anonymous_identity="ttls", | |
148 | password="password", phase2="auth=MSCHAPV2", | |
d7a99700 JM |
149 | ca_cert="auth_serv/ca.pem", wait_connect=False, |
150 | scan_freq="2412") | |
ebc61dc2 JM |
151 | req = dev[0].request("GAS_REQUEST " + bssid + " 00 000102000101") |
152 | if "FAIL" in req: | |
153 | raise Exception("GAS query request rejected") | |
154 | ||
155 | ev = dev[0].wait_event(["CTRL-EVENT-CONNECTED", "GAS-RESPONSE-INFO"], | |
156 | timeout=20) | |
157 | if ev is None: | |
158 | raise Exception("Operation timed out") | |
159 | if "CTRL-EVENT-CONNECTED" not in ev: | |
160 | raise Exception("Unexpected operation order") | |
161 | ||
162 | ev = dev[0].wait_event(["CTRL-EVENT-CONNECTED", "GAS-RESPONSE-INFO"], | |
163 | timeout=20) | |
164 | if ev is None: | |
165 | raise Exception("Operation timed out") | |
166 | if "GAS-RESPONSE-INFO" not in ev: | |
167 | raise Exception("Unexpected operation order") | |
168 | get_gas_response(dev[0], bssid, ev) | |
169 | ||
170 | dev[0].request("DISCONNECT") | |
171 | ev = dev[0].wait_event(["CTRL-EVENT-DISCONNECTED"], timeout=5) | |
172 | if ev is None: | |
173 | raise Exception("Disconnection timed out") | |
174 | ||
175 | logger.debug("Wait six seconds for expiration of connect-without-scan") | |
176 | time.sleep(6) | |
adf277a0 | 177 | dev[0].dump_monitor() |
ebc61dc2 JM |
178 | |
179 | logger.debug("Start concurrent GAS request and connect") | |
180 | req = dev[0].request("GAS_REQUEST " + bssid + " 00 000102000101") | |
181 | if "FAIL" in req: | |
182 | raise Exception("GAS query request rejected") | |
183 | dev[0].request("RECONNECT") | |
184 | ||
185 | ev = dev[0].wait_event(["GAS-RESPONSE-INFO"], timeout=10) | |
186 | if ev is None: | |
187 | raise Exception("Operation timed out") | |
188 | get_gas_response(dev[0], bssid, ev) | |
189 | ||
190 | ev = dev[0].wait_event(["CTRL-EVENT-SCAN-RESULTS"], timeout=20) | |
191 | if ev is None: | |
192 | raise Exception("No new scan results reported") | |
193 | ||
194 | ev = dev[0].wait_event(["CTRL-EVENT-CONNECTED"], timeout=20) | |
195 | if ev is None: | |
196 | raise Exception("Operation timed out") | |
197 | if "CTRL-EVENT-CONNECTED" not in ev: | |
198 | raise Exception("Unexpected operation order") | |
836a3745 JM |
199 | |
200 | def test_gas_fragment(dev, apdev): | |
201 | """GAS fragmentation""" | |
18dd2af0 | 202 | hapd = start_ap(apdev[0]) |
836a3745 JM |
203 | hapd.set("gas_frag_limit", "50") |
204 | ||
e4a44b3c JM |
205 | dev[0].scan(freq="2412") |
206 | dev[0].request("FETCH_ANQP") | |
cef16c47 JM |
207 | for i in range(0, 13): |
208 | ev = dev[0].wait_event(["RX-ANQP", "RX-HS20-ANQP"], timeout=5) | |
e4a44b3c JM |
209 | if ev is None: |
210 | raise Exception("Operation timed out") | |
211 | ||
212 | def test_gas_comeback_delay(dev, apdev): | |
213 | """GAS fragmentation""" | |
18dd2af0 | 214 | hapd = start_ap(apdev[0]) |
e4a44b3c JM |
215 | hapd.set("gas_comeback_delay", "500") |
216 | ||
217 | dev[0].scan(freq="2412") | |
836a3745 JM |
218 | dev[0].request("FETCH_ANQP") |
219 | for i in range(0, 6): | |
220 | ev = dev[0].wait_event(["RX-ANQP"], timeout=5) | |
221 | if ev is None: | |
222 | raise Exception("Operation timed out") | |
2cace98e | 223 | |
bfe375ec JM |
224 | def expect_gas_result(dev, result): |
225 | ev = dev.wait_event(["GAS-QUERY-DONE"], timeout=10) | |
226 | if ev is None: | |
227 | raise Exception("GAS query timed out") | |
228 | if "result=" + result not in ev: | |
229 | raise Exception("Unexpected GAS query result") | |
230 | ||
18dd2af0 JM |
231 | def anqp_get(dev, bssid, id): |
232 | dev.request("ANQP_GET " + bssid + " " + str(id)) | |
233 | ev = dev.wait_event(["GAS-QUERY-START"], timeout=5) | |
234 | if ev is None: | |
235 | raise Exception("GAS query start timed out") | |
236 | ||
2cace98e JM |
237 | def test_gas_timeout(dev, apdev): |
238 | """GAS timeout""" | |
18dd2af0 | 239 | hapd = start_ap(apdev[0]) |
2cace98e | 240 | bssid = apdev[0]['bssid'] |
2cace98e JM |
241 | |
242 | dev[0].scan(freq="2412") | |
243 | hapd.set("ext_mgmt_frame_handling", "1") | |
244 | ||
18dd2af0 | 245 | anqp_get(dev[0], bssid, 263) |
2cace98e JM |
246 | |
247 | ev = hapd.wait_event(["MGMT-RX"], timeout=5) | |
248 | if ev is None: | |
249 | raise Exception("MGMT RX wait timed out") | |
250 | ||
bfe375ec JM |
251 | expect_gas_result(dev[0], "TIMEOUT") |
252 | ||
d9474958 JM |
253 | MGMT_SUBTYPE_ACTION = 13 |
254 | ACTION_CATEG_PUBLIC = 4 | |
255 | ||
256 | GAS_INITIAL_REQUEST = 10 | |
257 | GAS_INITIAL_RESPONSE = 11 | |
258 | GAS_COMEBACK_REQUEST = 12 | |
259 | GAS_COMEBACK_RESPONSE = 13 | |
260 | GAS_ACTIONS = [ GAS_INITIAL_REQUEST, GAS_INITIAL_RESPONSE, | |
261 | GAS_COMEBACK_REQUEST, GAS_COMEBACK_RESPONSE ] | |
262 | ||
263 | def anqp_adv_proto(): | |
264 | return struct.pack('BBBB', 108, 2, 127, 0) | |
265 | ||
fe871e48 JM |
266 | def anqp_initial_resp(dialog_token, status_code): |
267 | return struct.pack('<BBBHH', ACTION_CATEG_PUBLIC, GAS_INITIAL_RESPONSE, | |
268 | dialog_token, status_code, 0) + anqp_adv_proto() | |
269 | ||
d9474958 JM |
270 | def anqp_comeback_resp(dialog_token): |
271 | return struct.pack('<BBBHBH', ACTION_CATEG_PUBLIC, GAS_COMEBACK_RESPONSE, | |
272 | dialog_token, 0, 0, 0) + anqp_adv_proto() | |
273 | ||
274 | def gas_rx(hapd): | |
275 | count = 0 | |
276 | while count < 30: | |
277 | count = count + 1 | |
278 | query = hapd.mgmt_rx() | |
279 | if query is None: | |
280 | raise Exception("Action frame not received") | |
281 | if query['subtype'] != MGMT_SUBTYPE_ACTION: | |
282 | continue | |
283 | payload = query['payload'] | |
284 | if len(payload) < 2: | |
285 | continue | |
286 | (category, action) = struct.unpack('BB', payload[0:2]) | |
287 | if category != ACTION_CATEG_PUBLIC or action not in GAS_ACTIONS: | |
288 | continue | |
289 | return query | |
290 | raise Exception("No Action frame received") | |
291 | ||
292 | def parse_gas(payload): | |
293 | pos = payload | |
294 | (category, action, dialog_token) = struct.unpack('BBB', pos[0:3]) | |
295 | if category != ACTION_CATEG_PUBLIC: | |
296 | return None | |
297 | if action not in GAS_ACTIONS: | |
298 | return None | |
299 | gas = {} | |
300 | gas['action'] = action | |
301 | pos = pos[3:] | |
302 | ||
303 | if len(pos) < 1: | |
304 | return None | |
305 | ||
306 | gas['dialog_token'] = dialog_token | |
307 | return gas | |
308 | ||
309 | def action_response(req): | |
310 | resp = {} | |
311 | resp['fc'] = req['fc'] | |
312 | resp['da'] = req['sa'] | |
313 | resp['sa'] = req['da'] | |
314 | resp['bssid'] = req['bssid'] | |
315 | return resp | |
316 | ||
bfe375ec JM |
317 | def test_gas_invalid_response_type(dev, apdev): |
318 | """GAS invalid response type""" | |
18dd2af0 | 319 | hapd = start_ap(apdev[0]) |
bfe375ec | 320 | bssid = apdev[0]['bssid'] |
bfe375ec JM |
321 | |
322 | dev[0].scan(freq="2412") | |
323 | hapd.set("ext_mgmt_frame_handling", "1") | |
324 | ||
18dd2af0 | 325 | anqp_get(dev[0], bssid, 263) |
bfe375ec | 326 | |
d9474958 JM |
327 | query = gas_rx(hapd) |
328 | gas = parse_gas(query['payload']) | |
329 | ||
330 | resp = action_response(query) | |
bfe375ec | 331 | # GAS Comeback Response instead of GAS Initial Response |
d9474958 | 332 | resp['payload'] = anqp_comeback_resp(gas['dialog_token']) + struct.pack('<H', 0) |
bfe375ec JM |
333 | hapd.mgmt_tx(resp) |
334 | ev = hapd.wait_event(["MGMT-TX-STATUS"], timeout=5) | |
335 | if ev is None: | |
336 | raise Exception("Missing TX status for GAS response") | |
337 | if "ok=1" not in ev: | |
338 | raise Exception("GAS response not acknowledged") | |
339 | ||
340 | # station drops the invalid frame, so this needs to result in GAS timeout | |
341 | expect_gas_result(dev[0], "TIMEOUT") | |
fe871e48 JM |
342 | |
343 | def test_gas_failure_status_code(dev, apdev): | |
344 | """GAS failure status code""" | |
345 | hapd = start_ap(apdev[0]) | |
346 | bssid = apdev[0]['bssid'] | |
347 | ||
348 | dev[0].scan(freq="2412") | |
349 | hapd.set("ext_mgmt_frame_handling", "1") | |
350 | ||
351 | anqp_get(dev[0], bssid, 263) | |
352 | ||
353 | query = gas_rx(hapd) | |
354 | gas = parse_gas(query['payload']) | |
355 | ||
356 | resp = action_response(query) | |
357 | resp['payload'] = anqp_initial_resp(gas['dialog_token'], 61) + struct.pack('<H', 0) | |
358 | hapd.mgmt_tx(resp) | |
359 | ev = hapd.wait_event(["MGMT-TX-STATUS"], timeout=5) | |
360 | if ev is None: | |
361 | raise Exception("Missing TX status for GAS response") | |
362 | if "ok=1" not in ev: | |
363 | raise Exception("GAS response not acknowledged") | |
364 | ||
365 | expect_gas_result(dev[0], "FAILURE") | |
84262fef JM |
366 | |
367 | def test_gas_malformed(dev, apdev): | |
368 | """GAS malformed response frames""" | |
369 | hapd = start_ap(apdev[0]) | |
370 | bssid = apdev[0]['bssid'] | |
371 | ||
372 | dev[0].scan(freq="2412") | |
373 | hapd.set("ext_mgmt_frame_handling", "1") | |
374 | ||
375 | anqp_get(dev[0], bssid, 263) | |
376 | ||
377 | query = gas_rx(hapd) | |
378 | gas = parse_gas(query['payload']) | |
379 | ||
380 | resp = action_response(query) | |
381 | ||
382 | resp['payload'] = struct.pack('<BBBH', ACTION_CATEG_PUBLIC, | |
383 | GAS_COMEBACK_RESPONSE, | |
384 | gas['dialog_token'], 0) | |
385 | hapd.mgmt_tx(resp) | |
386 | ||
387 | resp['payload'] = struct.pack('<BBBHB', ACTION_CATEG_PUBLIC, | |
388 | GAS_COMEBACK_RESPONSE, | |
389 | gas['dialog_token'], 0, 0) | |
390 | hapd.mgmt_tx(resp) | |
391 | ||
392 | hdr = struct.pack('<BBBHH', ACTION_CATEG_PUBLIC, GAS_INITIAL_RESPONSE, | |
393 | gas['dialog_token'], 0, 0) | |
394 | resp['payload'] = hdr + struct.pack('B', 108) | |
395 | hapd.mgmt_tx(resp) | |
396 | resp['payload'] = hdr + struct.pack('BB', 108, 0) | |
397 | hapd.mgmt_tx(resp) | |
398 | resp['payload'] = hdr + struct.pack('BB', 108, 1) | |
399 | hapd.mgmt_tx(resp) | |
400 | resp['payload'] = hdr + struct.pack('BB', 108, 255) | |
401 | hapd.mgmt_tx(resp) | |
402 | resp['payload'] = hdr + struct.pack('BBB', 108, 1, 127) | |
403 | hapd.mgmt_tx(resp) | |
404 | resp['payload'] = hdr + struct.pack('BBB', 108, 2, 127) | |
405 | hapd.mgmt_tx(resp) | |
406 | resp['payload'] = hdr + struct.pack('BBBB', 0, 2, 127, 0) | |
407 | hapd.mgmt_tx(resp) | |
408 | ||
409 | resp['payload'] = anqp_initial_resp(gas['dialog_token'], 0) + struct.pack('<H', 1) | |
410 | hapd.mgmt_tx(resp) | |
411 | ||
412 | resp['payload'] = anqp_initial_resp(gas['dialog_token'], 0) + struct.pack('<HB', 2, 0) | |
413 | hapd.mgmt_tx(resp) | |
414 | ||
415 | resp['payload'] = anqp_initial_resp(gas['dialog_token'], 0) + struct.pack('<H', 65535) | |
416 | hapd.mgmt_tx(resp) | |
417 | ||
418 | resp['payload'] = anqp_initial_resp(gas['dialog_token'], 0) + struct.pack('<HBB', 1, 0, 0) | |
419 | hapd.mgmt_tx(resp) | |
420 | ||
421 | # Station drops invalid frames, but the last of the responses is valid from | |
422 | # GAS view point even though it has an extra octet in the end and the ANQP | |
423 | # part of the response is not valid. This is reported as successfulyl | |
424 | # completed GAS exchange. | |
425 | expect_gas_result(dev[0], "SUCCESS") |