1#!/usr/bin/env python3 2# 3# Copyright 2019 - The Android Open Source Project 4# 5# Licensed under the Apache License, Version 2.0 (the 'License'); 6# you may not use this file except in compliance with the License. 7# You may obtain a copy of the License at 8# 9# http://www.apache.org/licenses/LICENSE-2.0 10# 11# Unless required by applicable law or agreed to in writing, software 12# distributed under the License is distributed on an 'AS IS' BASIS, 13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14# See the License for the specific language governing permissions and 15# limitations under the License. 16 17import csv 18import os 19import posixpath 20import time 21import acts.test_utils.wifi.wifi_test_utils as wutils 22 23from acts import context 24from acts import logger 25from acts import utils 26from acts.controllers.utils_lib import ssh 27 28WifiEnums = wutils.WifiEnums 29SNIFFER_TIMEOUT = 6 30 31 32def create(configs): 33 """Factory method for sniffer. 34 Args: 35 configs: list of dicts with sniffer settings. 36 Settings must contain the following : ssh_settings, type, OS, interface. 37 38 Returns: 39 objs: list of sniffer class objects. 40 """ 41 objs = [] 42 for config in configs: 43 try: 44 if config['type'] == 'tshark': 45 if config['os'] == 'unix': 46 objs.append(TsharkSnifferOnUnix(config)) 47 elif config['os'] == 'linux': 48 objs.append(TsharkSnifferOnLinux(config)) 49 else: 50 raise RuntimeError('Wrong sniffer config') 51 52 elif config['type'] == 'mock': 53 objs.append(MockSniffer(config)) 54 except KeyError: 55 raise KeyError('Invalid sniffer configurations') 56 return objs 57 58 59def destroy(objs): 60 return 61 62 63class OtaSnifferBase(object): 64 """Base class defining common sniffers functions.""" 65 66 _log_file_counter = 0 67 68 @property 69 def started(self): 70 raise NotImplementedError('started must be specified.') 71 72 def start_capture(self, network, duration=30): 73 """Starts the sniffer Capture. 74 75 Args: 76 network: dict containing network information such as SSID, etc. 77 duration: duration of sniffer capture in seconds. 78 """ 79 raise NotImplementedError('start_capture must be specified.') 80 81 def stop_capture(self, tag=''): 82 """Stops the sniffer Capture. 83 84 Args: 85 tag: string to tag sniffer capture file name with. 86 """ 87 raise NotImplementedError('stop_capture must be specified.') 88 89 def _get_remote_dump_path(self): 90 """Returns name of the sniffer dump file.""" 91 remote_file_name = 'sniffer_dump.{}'.format( 92 self.sniffer_output_file_type) 93 remote_dump_path = posixpath.join(posixpath.sep, 'tmp', remote_file_name) 94 return remote_dump_path 95 96 def _get_full_file_path(self, tag=None): 97 """Returns the full file path for the sniffer capture dump file. 98 99 Returns the full file path (on test machine) for the sniffer capture 100 dump file. 101 102 Args: 103 tag: The tag appended to the sniffer capture dump file . 104 """ 105 tags = [tag, 'count', OtaSnifferBase._log_file_counter] 106 out_file_name = 'Sniffer_Capture_%s.%s' % ('_'.join([ 107 str(x) for x in tags if x != '' and x is not None 108 ]), self.sniffer_output_file_type) 109 OtaSnifferBase._log_file_counter += 1 110 111 file_path = os.path.join(self.log_path, out_file_name) 112 return file_path 113 114 @property 115 def log_path(self): 116 current_context = context.get_current_context() 117 full_out_dir = os.path.join(current_context.get_full_output_path(), 118 'sniffer_captures') 119 120 # Ensure the directory exists. 121 os.makedirs(full_out_dir, exist_ok=True) 122 123 return full_out_dir 124 125 126class MockSniffer(OtaSnifferBase): 127 """Class that implements mock sniffer for test development and debug.""" 128 def __init__(self, config): 129 self.log = logger.create_tagged_trace_logger('Mock Sniffer') 130 131 def start_capture(self, network, duration=30): 132 """Starts sniffer capture on the specified machine. 133 134 Args: 135 network: dict of network credentials. 136 duration: duration of the sniff. 137 """ 138 self.log.info('Starting sniffer.') 139 140 def stop_capture(self): 141 """Stops the sniffer. 142 143 Returns: 144 log_file: name of processed sniffer. 145 """ 146 147 self.log.info('Stopping sniffer.') 148 log_file = self._get_full_file_path() 149 with open(log_file, 'w') as file: 150 file.write('this is a sniffer dump.') 151 return log_file 152 153 154class TsharkSnifferBase(OtaSnifferBase): 155 """Class that implements Tshark based sniffer controller. """ 156 157 TYPE_SUBTYPE_DICT = { 158 '0': 'Association Requests', 159 '1': 'Association Responses', 160 '2': 'Reassociation Requests', 161 '3': 'Resssociation Responses', 162 '4': 'Probe Requests', 163 '5': 'Probe Responses', 164 '8': 'Beacon', 165 '9': 'ATIM', 166 '10': 'Disassociations', 167 '11': 'Authentications', 168 '12': 'Deauthentications', 169 '13': 'Actions', 170 '24': 'Block ACK Requests', 171 '25': 'Block ACKs', 172 '26': 'PS-Polls', 173 '27': 'RTS', 174 '28': 'CTS', 175 '29': 'ACK', 176 '30': 'CF-Ends', 177 '31': 'CF-Ends/CF-Acks', 178 '32': 'Data', 179 '33': 'Data+CF-Ack', 180 '34': 'Data+CF-Poll', 181 '35': 'Data+CF-Ack+CF-Poll', 182 '36': 'Null', 183 '37': 'CF-Ack', 184 '38': 'CF-Poll', 185 '39': 'CF-Ack+CF-Poll', 186 '40': 'QoS Data', 187 '41': 'QoS Data+CF-Ack', 188 '42': 'QoS Data+CF-Poll', 189 '43': 'QoS Data+CF-Ack+CF-Poll', 190 '44': 'QoS Null', 191 '46': 'QoS CF-Poll (Null)', 192 '47': 'QoS CF-Ack+CF-Poll (Null)' 193 } 194 195 TSHARK_COLUMNS = [ 196 'frame_number', 'frame_time_relative', 'mactime', 'frame_len', 'rssi', 197 'channel', 'ta', 'ra', 'bssid', 'type', 'subtype', 'duration', 'seq', 198 'retry', 'pwrmgmt', 'moredata', 'ds', 'phy', 'radio_datarate', 199 'vht_datarate', 'radiotap_mcs_index', 'vht_mcs', 'wlan_data_rate', 200 '11n_mcs_index', '11ac_mcs', '11n_bw', '11ac_bw', 'vht_nss', 'mcs_gi', 201 'vht_gi', 'vht_coding', 'ba_bm', 'fc_status', 'bf_report' 202 ] 203 204 TSHARK_OUTPUT_COLUMNS = [ 205 'frame_number', 'frame_time_relative', 'mactime', 'ta', 'ra', 'bssid', 206 'rssi', 'channel', 'frame_len', 'Info', 'radio_datarate', 207 'radiotap_mcs_index', 'pwrmgmt', 'phy', 'vht_nss', 'vht_mcs', 208 'vht_datarate', '11ac_mcs', '11ac_bw', 'vht_gi', 'vht_coding', 209 'wlan_data_rate', '11n_mcs_index', '11n_bw', 'mcs_gi', 'type', 210 'subtype', 'duration', 'seq', 'retry', 'moredata', 'ds', 'ba_bm', 211 'fc_status', 'bf_report' 212 ] 213 214 TSHARK_FIELDS_LIST = [ 215 'frame.number', 'frame.time_relative', 'radiotap.mactime', 'frame.len', 216 'radiotap.dbm_antsignal', 'wlan_radio.channel', 'wlan.ta', 'wlan.ra', 217 'wlan.bssid', 'wlan.fc.type', 'wlan.fc.type_subtype', 'wlan.duration', 218 'wlan.seq', 'wlan.fc.retry', 'wlan.fc.pwrmgt', 'wlan.fc.moredata', 219 'wlan.fc.ds', 'wlan_radio.phy', 'radiotap.datarate', 220 'radiotap.vht.datarate.0', 'radiotap.mcs.index', 'radiotap.vht.mcs.0', 221 'wlan_radio.data_rate', 'wlan_radio.11n.mcs_index', 222 'wlan_radio.11ac.mcs', 'wlan_radio.11n.bandwidth', 223 'wlan_radio.11ac.bandwidth', 'radiotap.vht.nss.0', 'radiotap.mcs.gi', 224 'radiotap.vht.gi', 'radiotap.vht.coding.0', 'wlan.ba.bm', 225 'wlan.fcs.status', 'wlan.vht.compressed_beamforming_report.snr' 226 ] 227 228 def __init__(self, config): 229 self.sniffer_proc_pid = None 230 self.log = logger.create_tagged_trace_logger('Tshark Sniffer') 231 self.ssh_config = config['ssh_config'] 232 self.sniffer_os = config['os'] 233 self.run_as_sudo = config.get('run_as_sudo', False) 234 self.sniffer_output_file_type = config['output_file_type'] 235 self.sniffer_snap_length = config['snap_length'] 236 self.sniffer_interface = config['interface'] 237 238 #Logging into sniffer 239 self.log.info('Logging into sniffer.') 240 self._sniffer_server = ssh.connection.SshConnection( 241 ssh.settings.from_config(self.ssh_config)) 242 # Get tshark params 243 self.tshark_fields = self._generate_tshark_fields( 244 self.TSHARK_FIELDS_LIST) 245 self.tshark_path = self._sniffer_server.run('which tshark').stdout 246 247 @property 248 def _started(self): 249 return self.sniffer_proc_pid is not None 250 251 def _scan_for_networks(self): 252 """Scans for wireless networks on the sniffer.""" 253 raise NotImplementedError 254 255 def _get_tshark_command(self, duration): 256 """Frames the appropriate tshark command. 257 258 Args: 259 duration: duration to sniff for. 260 261 Returns: 262 tshark_command : appropriate tshark command. 263 """ 264 tshark_command = '{} -l -i {} -I -t u -a duration:{}'.format( 265 self.tshark_path, self.sniffer_interface, int(duration)) 266 if self.run_as_sudo: 267 tshark_command = 'sudo {}'.format(tshark_command) 268 269 return tshark_command 270 271 def _get_sniffer_command(self, tshark_command): 272 """ 273 Frames the appropriate sniffer command. 274 275 Args: 276 tshark_command: framed tshark command 277 278 Returns: 279 sniffer_command: appropriate sniffer command 280 """ 281 if self.sniffer_output_file_type in ['pcap', 'pcapng']: 282 sniffer_command = ' {tshark} -s {snaplength} -w {log_file} '.format( 283 tshark=tshark_command, 284 snaplength=self.sniffer_snap_length, 285 log_file=self._get_remote_dump_path()) 286 287 elif self.sniffer_output_file_type == 'csv': 288 sniffer_command = '{tshark} {fields} > {log_file}'.format( 289 tshark=tshark_command, 290 fields=self.tshark_fields, 291 log_file=self._get_remote_dump_path()) 292 293 else: 294 raise KeyError('Sniffer output file type not configured correctly') 295 296 return sniffer_command 297 298 def _generate_tshark_fields(self, fields): 299 """Generates tshark fields to be appended to the tshark command. 300 301 Args: 302 fields: list of tshark fields to be appended to the tshark command. 303 304 Returns: 305 tshark_fields: string of tshark fields to be appended 306 to the tshark command. 307 """ 308 tshark_fields = "-T fields -y IEEE802_11_RADIO -E separator='^'" 309 for field in fields: 310 tshark_fields = tshark_fields + ' -e {}'.format(field) 311 return tshark_fields 312 313 def _configure_sniffer(self, network, chan, bw): 314 """ Connects to a wireless network using networksetup utility. 315 316 Args: 317 network: dictionary of network credentials; SSID and password. 318 """ 319 raise NotImplementedError 320 321 def _run_tshark(self, sniffer_command): 322 """Starts the sniffer. 323 324 Args: 325 sniffer_command: sniffer command to execute. 326 """ 327 self.log.info('Starting sniffer.') 328 sniffer_job = self._sniffer_server.run_async(sniffer_command) 329 self.sniffer_proc_pid = sniffer_job.stdout 330 331 def _stop_tshark(self): 332 """ Stops the sniffer.""" 333 self.log.info('Stopping sniffer') 334 335 # while loop to kill the sniffer process 336 stop_time = time.time() + SNIFFER_TIMEOUT 337 while time.time() < stop_time: 338 # Wait before sending more kill signals 339 time.sleep(0.1) 340 try: 341 # Returns 1 if process was killed 342 self._sniffer_server.run( 343 'ps aux| grep {} | grep -v grep'.format( 344 self.sniffer_proc_pid)) 345 except: 346 return 347 try: 348 # Returns error if process was killed already 349 self._sniffer_server.run('sudo kill -15 {}'.format( 350 str(self.sniffer_proc_pid))) 351 except: 352 # Except is hit when tshark is already dead but we will break 353 # out of the loop when confirming process is dead using ps aux 354 pass 355 self.log.warning('Could not stop sniffer. Trying with SIGKILL.') 356 try: 357 self.log.debug('Killing sniffer with SIGKILL.') 358 self._sniffer_server.run('sudo kill -9 {}'.format( 359 str(self.sniffer_proc_pid))) 360 except: 361 self.log.debug('Sniffer process may have stopped succesfully.') 362 363 def _process_tshark_dump(self, log_file): 364 """ Process tshark dump for better readability. 365 366 Processes tshark dump for better readability and saves it to a file. 367 Adds an info column at the end of each row. Format of the info columns: 368 subtype of the frame, sequence no and retry status. 369 370 Args: 371 log_file : unprocessed sniffer output 372 Returns: 373 log_file : processed sniffer output 374 """ 375 temp_dump_file = os.path.join(self.log_path, 'sniffer_temp_dump.csv') 376 utils.exe_cmd('cp {} {}'.format(log_file, temp_dump_file)) 377 378 with open(temp_dump_file, 'r') as input_csv, open(log_file, 379 'w') as output_csv: 380 reader = csv.DictReader(input_csv, 381 fieldnames=self.TSHARK_COLUMNS, 382 delimiter='^') 383 writer = csv.DictWriter(output_csv, 384 fieldnames=self.TSHARK_OUTPUT_COLUMNS, 385 delimiter='\t') 386 writer.writeheader() 387 for row in reader: 388 if row['subtype'] in self.TYPE_SUBTYPE_DICT: 389 row['Info'] = '{sub} S={seq} retry={retry_status}'.format( 390 sub=self.TYPE_SUBTYPE_DICT[row['subtype']], 391 seq=row['seq'], 392 retry_status=row['retry']) 393 else: 394 row['Info'] = '{} S={} retry={}\n'.format( 395 row['subtype'], row['seq'], row['retry']) 396 writer.writerow(row) 397 398 utils.exe_cmd('rm -f {}'.format(temp_dump_file)) 399 return log_file 400 401 def start_capture(self, network, chan, bw, duration=60): 402 """Starts sniffer capture on the specified machine. 403 404 Args: 405 network: dict describing network to sniff on. 406 duration: duration of sniff. 407 """ 408 # Checking for existing sniffer processes 409 if self._started: 410 self.log.info('Sniffer already running') 411 return 412 413 # Configure sniffer 414 self._configure_sniffer(network, chan, bw) 415 tshark_command = self._get_tshark_command(duration) 416 sniffer_command = self._get_sniffer_command(tshark_command) 417 418 # Starting sniffer capture by executing tshark command 419 self._run_tshark(sniffer_command) 420 421 def stop_capture(self, tag=''): 422 """Stops the sniffer. 423 424 Args: 425 tag: tag to be appended to the sniffer output file. 426 Returns: 427 log_file: path to sniffer dump. 428 """ 429 # Checking if there is an ongoing sniffer capture 430 if not self._started: 431 self.log.error('No sniffer process running') 432 return 433 # Killing sniffer process 434 self._stop_tshark() 435 436 # Processing writing capture output to file 437 log_file = self._get_full_file_path(tag) 438 self._sniffer_server.run('sudo chmod 777 {}'.format( 439 self._get_remote_dump_path())) 440 self._sniffer_server.pull_file(log_file, self._get_remote_dump_path()) 441 442 if self.sniffer_output_file_type == 'csv': 443 log_file = self._process_tshark_dump(log_file) 444 445 self.sniffer_proc_pid = None 446 return log_file 447 448 449class TsharkSnifferOnUnix(TsharkSnifferBase): 450 """Class that implements Tshark based sniffer controller on Unix systems.""" 451 def _scan_for_networks(self): 452 """Scans the wireless networks on the sniffer. 453 454 Returns: 455 scan_results : output of the scan command. 456 """ 457 scan_command = '/usr/local/bin/airport -s' 458 scan_result = self._sniffer_server.run(scan_command).stdout 459 460 return scan_result 461 462 def _configure_sniffer(self, network, chan, bw): 463 """Connects to a wireless network using networksetup utility. 464 465 Args: 466 network: dictionary of network credentials; SSID and password. 467 """ 468 469 self.log.debug('Connecting to network {}'.format(network['SSID'])) 470 471 if 'password' not in network: 472 network['password'] = '' 473 474 connect_command = 'networksetup -setairportnetwork en0 {} {}'.format( 475 network['SSID'], network['password']) 476 self._sniffer_server.run(connect_command) 477 478 479class TsharkSnifferOnLinux(TsharkSnifferBase): 480 """Class that implements Tshark based sniffer controller on Linux.""" 481 def __init__(self, config): 482 super().__init__(config) 483 self._init_sniffer() 484 self.channel = None 485 self.bandwidth = None 486 487 def _init_sniffer(self): 488 """Function to configure interface for the first time""" 489 self._sniffer_server.run('sudo modprobe -r iwlwifi') 490 self._sniffer_server.run('sudo dmesg -C') 491 self._sniffer_server.run('cat /dev/null | sudo tee /var/log/syslog') 492 self._sniffer_server.run('sudo modprobe iwlwifi debug=0x1') 493 # Wait for wifi config changes before trying to further configuration 494 # e.g. setting monitor mode (which will fail if above is not complete) 495 time.sleep(1) 496 497 def set_monitor_mode(self, chan, bw): 498 """Function to configure interface to monitor mode 499 500 Brings up the sniffer wireless interface in monitor mode and 501 tunes it to the appropriate channel and bandwidth 502 503 Args: 504 chan: primary channel (int) to tune the sniffer to 505 bw: bandwidth (int) to tune the sniffer to 506 """ 507 if chan == self.channel and bw == self.bandwidth: 508 return 509 510 self.channel = chan 511 self.bandwidth = bw 512 513 channel_map = { 514 80: { 515 tuple(range(36, 50, 2)): 42, 516 tuple(range(52, 66, 2)): 58, 517 tuple(range(100, 114, 2)): 106, 518 tuple(range(116, 130, 2)): 122, 519 tuple(range(132, 146, 2)): 138, 520 tuple(range(149, 163, 2)): 155 521 }, 522 40: { 523 (36, 38, 40): 38, 524 (44, 46, 48): 46, 525 (52, 54, 56): 54, 526 (60, 62, 64): 62, 527 (100, 102, 104): 102, 528 (108, 110, 112): 108, 529 (116, 118, 120): 118, 530 (124, 126, 128): 126, 531 (132, 134, 136): 134, 532 (140, 142, 144): 142, 533 (149, 151, 153): 151, 534 (157, 159, 161): 159 535 } 536 } 537 538 if chan <= 13: 539 primary_freq = WifiEnums.channel_2G_to_freq[chan] 540 else: 541 primary_freq = WifiEnums.channel_5G_to_freq[chan] 542 543 self._sniffer_server.run('sudo ifconfig {} down'.format( 544 self.sniffer_interface)) 545 self._sniffer_server.run('sudo iwconfig {} mode monitor'.format( 546 self.sniffer_interface)) 547 self._sniffer_server.run('sudo ifconfig {} up'.format( 548 self.sniffer_interface)) 549 550 if bw in channel_map: 551 for tuple_chan in channel_map[bw]: 552 if chan in tuple_chan: 553 center_freq = WifiEnums.channel_5G_to_freq[channel_map[bw] 554 [tuple_chan]] 555 self._sniffer_server.run( 556 'sudo iw dev {} set freq {} {} {}'.format( 557 self.sniffer_interface, primary_freq, bw, 558 center_freq)) 559 560 else: 561 self._sniffer_server.run('sudo iw dev {} set freq {}'.format( 562 self.sniffer_interface, primary_freq)) 563 564 def _configure_sniffer(self, network, chan, bw): 565 """ Connects to a wireless network using networksetup utility. 566 567 Args: 568 network: dictionary of network credentials; SSID and password. 569 """ 570 571 self.log.debug('Connecting to network {}'.format(network['SSID'])) 572 self.set_monitor_mode(chan, bw) 573