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.info('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.info('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 239 #Logging into sniffer 240 self.log.info('Logging into sniffer.') 241 self._sniffer_server = ssh.connection.SshConnection( 242 ssh.settings.from_config(self.ssh_config)) 243 # Get tshark params 244 self.tshark_fields = self._generate_tshark_fields( 245 self.TSHARK_FIELDS_LIST) 246 self.tshark_path = self._sniffer_server.run('which tshark').stdout 247 248 @property 249 def _started(self): 250 return self.sniffer_proc_pid is not None 251 252 def _scan_for_networks(self): 253 """Scans for wireless networks on the sniffer.""" 254 raise NotImplementedError 255 256 def _get_tshark_command(self, duration): 257 """Frames the appropriate tshark command. 258 259 Args: 260 duration: duration to sniff for. 261 262 Returns: 263 tshark_command : appropriate tshark command. 264 """ 265 tshark_command = '{} -l -i {} -I -t u -a duration:{}'.format( 266 self.tshark_path, self.sniffer_interface, int(duration)) 267 if self.run_as_sudo: 268 tshark_command = 'sudo {}'.format(tshark_command) 269 270 return tshark_command 271 272 def _get_sniffer_command(self, tshark_command): 273 """ 274 Frames the appropriate sniffer command. 275 276 Args: 277 tshark_command: framed tshark command 278 279 Returns: 280 sniffer_command: appropriate sniffer command 281 """ 282 if self.sniffer_output_file_type in ['pcap', 'pcapng']: 283 sniffer_command = ' {tshark} -s {snaplength} -w {log_file} '.format( 284 tshark=tshark_command, 285 snaplength=self.sniffer_snap_length, 286 log_file=self._get_remote_dump_path()) 287 288 elif self.sniffer_output_file_type == 'csv': 289 sniffer_command = '{tshark} {fields} > {log_file}'.format( 290 tshark=tshark_command, 291 fields=self.tshark_fields, 292 log_file=self._get_remote_dump_path()) 293 294 else: 295 raise KeyError('Sniffer output file type not configured correctly') 296 297 return sniffer_command 298 299 def _generate_tshark_fields(self, fields): 300 """Generates tshark fields to be appended to the tshark command. 301 302 Args: 303 fields: list of tshark fields to be appended to the tshark command. 304 305 Returns: 306 tshark_fields: string of tshark fields to be appended 307 to the tshark command. 308 """ 309 tshark_fields = "-T fields -y IEEE802_11_RADIO -E separator='^'" 310 for field in fields: 311 tshark_fields = tshark_fields + ' -e {}'.format(field) 312 return tshark_fields 313 314 def _configure_sniffer(self, network, chan, bw): 315 """ Connects to a wireless network using networksetup utility. 316 317 Args: 318 network: dictionary of network credentials; SSID and password. 319 """ 320 raise NotImplementedError 321 322 def _run_tshark(self, sniffer_command): 323 """Starts the sniffer. 324 325 Args: 326 sniffer_command: sniffer command to execute. 327 """ 328 self.log.info('Starting sniffer.') 329 sniffer_job = self._sniffer_server.run_async(sniffer_command) 330 self.sniffer_proc_pid = sniffer_job.stdout 331 332 def _stop_tshark(self): 333 """ Stops the sniffer.""" 334 self.log.info('Stopping sniffer') 335 336 # while loop to kill the sniffer process 337 stop_time = time.time() + SNIFFER_TIMEOUT 338 while time.time() < stop_time: 339 # Wait before sending more kill signals 340 time.sleep(0.1) 341 try: 342 # Returns 1 if process was killed 343 self._sniffer_server.run( 344 'ps aux| grep {} | grep -v grep'.format( 345 self.sniffer_proc_pid)) 346 except: 347 return 348 try: 349 # Returns error if process was killed already 350 self._sniffer_server.run('sudo kill -15 {}'.format( 351 str(self.sniffer_proc_pid))) 352 except: 353 # Except is hit when tshark is already dead but we will break 354 # out of the loop when confirming process is dead using ps aux 355 pass 356 self.log.warning('Could not stop sniffer. Trying with SIGKILL.') 357 try: 358 self.log.debug('Killing sniffer with SIGKILL.') 359 self._sniffer_server.run('sudo kill -9 {}'.format( 360 str(self.sniffer_proc_pid))) 361 except: 362 self.log.debug('Sniffer process may have stopped succesfully.') 363 364 def _process_tshark_dump(self, log_file): 365 """ Process tshark dump for better readability. 366 367 Processes tshark dump for better readability and saves it to a file. 368 Adds an info column at the end of each row. Format of the info columns: 369 subtype of the frame, sequence no and retry status. 370 371 Args: 372 log_file : unprocessed sniffer output 373 Returns: 374 log_file : processed sniffer output 375 """ 376 temp_dump_file = os.path.join(self.log_path, 'sniffer_temp_dump.csv') 377 utils.exe_cmd('cp {} {}'.format(log_file, temp_dump_file)) 378 379 with open(temp_dump_file, 'r') as input_csv, open(log_file, 380 'w') as output_csv: 381 reader = csv.DictReader(input_csv, 382 fieldnames=self.TSHARK_COLUMNS, 383 delimiter='^') 384 writer = csv.DictWriter(output_csv, 385 fieldnames=self.TSHARK_OUTPUT_COLUMNS, 386 delimiter='\t') 387 writer.writeheader() 388 for row in reader: 389 if row['subtype'] in self.TYPE_SUBTYPE_DICT: 390 row['Info'] = '{sub} S={seq} retry={retry_status}'.format( 391 sub=self.TYPE_SUBTYPE_DICT[row['subtype']], 392 seq=row['seq'], 393 retry_status=row['retry']) 394 else: 395 row['Info'] = '{} S={} retry={}\n'.format( 396 row['subtype'], row['seq'], row['retry']) 397 writer.writerow(row) 398 399 utils.exe_cmd('rm -f {}'.format(temp_dump_file)) 400 return log_file 401 402 def start_capture(self, network, chan, bw, duration=60): 403 """Starts sniffer capture on the specified machine. 404 405 Args: 406 network: dict describing network to sniff on. 407 duration: duration of sniff. 408 """ 409 # Checking for existing sniffer processes 410 if self._started: 411 self.log.info('Sniffer already running') 412 return 413 414 # Configure sniffer 415 self._configure_sniffer(network, chan, bw) 416 tshark_command = self._get_tshark_command(duration) 417 sniffer_command = self._get_sniffer_command(tshark_command) 418 419 # Starting sniffer capture by executing tshark command 420 self._run_tshark(sniffer_command) 421 422 def stop_capture(self, tag=''): 423 """Stops the sniffer. 424 425 Args: 426 tag: tag to be appended to the sniffer output file. 427 Returns: 428 log_file: path to sniffer dump. 429 """ 430 # Checking if there is an ongoing sniffer capture 431 if not self._started: 432 self.log.error('No sniffer process running') 433 return 434 # Killing sniffer process 435 self._stop_tshark() 436 437 # Processing writing capture output to file 438 log_file = self._get_full_file_path(tag) 439 self._sniffer_server.run('sudo chmod 777 {}'.format( 440 self._get_remote_dump_path())) 441 self._sniffer_server.pull_file(log_file, self._get_remote_dump_path()) 442 443 if self.sniffer_output_file_type == 'csv': 444 log_file = self._process_tshark_dump(log_file) 445 446 self.sniffer_proc_pid = None 447 return log_file 448 449 450class TsharkSnifferOnUnix(TsharkSnifferBase): 451 """Class that implements Tshark based sniffer controller on Unix systems.""" 452 def _scan_for_networks(self): 453 """Scans the wireless networks on the sniffer. 454 455 Returns: 456 scan_results : output of the scan command. 457 """ 458 scan_command = '/usr/local/bin/airport -s' 459 scan_result = self._sniffer_server.run(scan_command).stdout 460 461 return scan_result 462 463 def _configure_sniffer(self, network, chan, bw): 464 """Connects to a wireless network using networksetup utility. 465 466 Args: 467 network: dictionary of network credentials; SSID and password. 468 """ 469 470 self.log.debug('Connecting to network {}'.format(network['SSID'])) 471 472 if 'password' not in network: 473 network['password'] = '' 474 475 connect_command = 'networksetup -setairportnetwork en0 {} {}'.format( 476 network['SSID'], network['password']) 477 self._sniffer_server.run(connect_command) 478 479 480class TsharkSnifferOnLinux(TsharkSnifferBase): 481 """Class that implements Tshark based sniffer controller on Linux.""" 482 def __init__(self, config): 483 super().__init__(config) 484 self._init_sniffer() 485 self.channel = None 486 self.bandwidth = None 487 488 def _init_sniffer(self): 489 """Function to configure interface for the first time""" 490 self._sniffer_server.run('sudo modprobe -r iwlwifi') 491 self._sniffer_server.run('sudo dmesg -C') 492 self._sniffer_server.run('cat /dev/null | sudo tee /var/log/syslog') 493 self._sniffer_server.run('sudo modprobe iwlwifi debug=0x1') 494 # Wait for wifi config changes before trying to further configuration 495 # e.g. setting monitor mode (which will fail if above is not complete) 496 time.sleep(1) 497 498 def set_monitor_mode(self, chan, bw): 499 """Function to configure interface to monitor mode 500 501 Brings up the sniffer wireless interface in monitor mode and 502 tunes it to the appropriate channel and bandwidth 503 504 Args: 505 chan: primary channel (int) to tune the sniffer to 506 bw: bandwidth (int) to tune the sniffer to 507 """ 508 if chan == self.channel and bw == self.bandwidth: 509 return 510 511 self.channel = chan 512 self.bandwidth = bw 513 514 channel_map = { 515 80: { 516 tuple(range(36, 50, 2)): 42, 517 tuple(range(52, 66, 2)): 58, 518 tuple(range(100, 114, 2)): 106, 519 tuple(range(116, 130, 2)): 122, 520 tuple(range(132, 146, 2)): 138, 521 tuple(range(149, 163, 2)): 155 522 }, 523 40: { 524 (36, 38, 40): 38, 525 (44, 46, 48): 46, 526 (52, 54, 56): 54, 527 (60, 62, 64): 62, 528 (100, 102, 104): 102, 529 (108, 110, 112): 108, 530 (116, 118, 120): 118, 531 (124, 126, 128): 126, 532 (132, 134, 136): 134, 533 (140, 142, 144): 142, 534 (149, 151, 153): 151, 535 (157, 159, 161): 159 536 }, 537 160: { 538 (36, 38, 40): 50 539 } 540 } 541 542 if chan <= 13: 543 primary_freq = WifiEnums.channel_2G_to_freq[chan] 544 else: 545 primary_freq = WifiEnums.channel_5G_to_freq[chan] 546 547 self._sniffer_server.run('sudo ifconfig {} down'.format( 548 self.sniffer_interface)) 549 self._sniffer_server.run('sudo iwconfig {} mode monitor'.format( 550 self.sniffer_interface)) 551 self._sniffer_server.run('sudo ifconfig {} up'.format( 552 self.sniffer_interface)) 553 554 if bw in channel_map: 555 for tuple_chan in channel_map[bw]: 556 if chan in tuple_chan: 557 center_freq = WifiEnums.channel_5G_to_freq[channel_map[bw] 558 [tuple_chan]] 559 self._sniffer_server.run( 560 'sudo iw dev {} set freq {} {} {}'.format( 561 self.sniffer_interface, primary_freq, bw, 562 center_freq)) 563 564 else: 565 self._sniffer_server.run('sudo iw dev {} set freq {}'.format( 566 self.sniffer_interface, primary_freq)) 567 568 def _configure_sniffer(self, network, chan, bw): 569 """ Connects to a wireless network using networksetup utility. 570 571 Args: 572 network: dictionary of network credentials; SSID and password. 573 """ 574 575 self.log.debug('Setting monitor mode on Ch {}, bw {}'.format(chan, bw)) 576 self.set_monitor_mode(chan, bw) 577