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