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