1# Lint as: python2, python3 2# Copyright (c) 2013 The Chromium OS Authors. All rights reserved. 3# Use of this source code is governed by a BSD-style license that can be 4# found in the LICENSE file. 5 6import collections 7import logging 8import os.path 9import time 10import uuid 11 12from autotest_lib.client.bin import utils 13from autotest_lib.client.common_lib import error 14from autotest_lib.client.common_lib.cros import path_utils 15from autotest_lib.client.common_lib.cros.network import iw_runner 16 17 18class PacketCapturesDisabledError(Exception): 19 """Signifies that this remote host does not support packet captures.""" 20 pass 21 22 23# local_pcap_path refers to the path of the result on the local host. 24# local_log_path refers to the tcpdump log file path on the local host. 25CaptureResult = collections.namedtuple('CaptureResult', 26 ['local_pcap_path', 'local_log_path']) 27 28# The number of bytes needed for a probe request is hard to define, 29# because the frame contents are variable (e.g. radiotap header may 30# contain different fields, maybe SSID isn't the first tagged 31# parameter?). The value here is 2x the largest frame size observed in 32# a quick sample. 33SNAPLEN_WIFI_PROBE_REQUEST = 600 34 35TCPDUMP_START_TIMEOUT_SECONDS = 5 36TCPDUMP_START_POLL_SECONDS = 0.1 37 38# These are WidthType objects from iw_runner 39WIDTH_HT20 = iw_runner.WIDTH_HT20 40WIDTH_HT40_PLUS = iw_runner.WIDTH_HT40_PLUS 41WIDTH_HT40_MINUS = iw_runner.WIDTH_HT40_MINUS 42WIDTH_VHT80 = iw_runner.WIDTH_VHT80 43WIDTH_VHT160 = iw_runner.WIDTH_VHT160 44WIDTH_VHT80_80 = iw_runner.WIDTH_VHT80_80 45 46_WIDTH_STRINGS = { 47 WIDTH_HT20: 'HT20', 48 WIDTH_HT40_PLUS: 'HT40+', 49 WIDTH_HT40_MINUS: 'HT40-', 50 WIDTH_VHT80: '80', 51 WIDTH_VHT160: '160', 52 WIDTH_VHT80_80: '80+80', 53} 54 55def _get_width_string(width): 56 """Returns a valid width parameter for "iw dev ${DEV} set freq". 57 58 @param width object, one of WIDTH_* 59 @return string iw readable width, or empty string 60 61 """ 62 return _WIDTH_STRINGS.get(width, '') 63 64 65def _get_center_freq_80(frequency): 66 """Find the center frequency of a 80MHz channel. 67 68 Raises an error upon an invalid frequency. 69 70 @param frequency int Control frequency of the channel. 71 @return center_freq int Center frequency of the channel. 72 73 """ 74 vht80 = [ 5180, 5260, 5500, 5580, 5660, 5745 ] 75 for f in vht80: 76 if frequency >= f and frequency < f + 80: 77 return f + 30 78 raise error.TestError( 79 'Frequency %s is not part of a 80MHz channel', frequency) 80 81 82def _get_center_freq_160(frequency): 83 """Find the center frequency of a 160MHz channel. 84 85 Raises an error upon an invalid frequency. 86 87 @param frequency int Control frequency of the channel. 88 @return center_freq int Center frequency of the channel. 89 90 """ 91 if (frequency >= 5180 and frequency <= 5320): 92 return 5250 93 if (frequency >= 5500 and frequency <= 5640): 94 return 5570 95 raise error.TestError( 96 'Frequency %s is not part of a 160MHz channel', frequency) 97 98 99def get_packet_capturer(host, host_description=None, cmd_ip=None, cmd_iw=None, 100 cmd_netdump=None, ignore_failures=False, logdir=None): 101 cmd_iw = cmd_iw or path_utils.get_install_path('iw', host=host) 102 cmd_ip = cmd_ip or path_utils.get_install_path('ip', host=host) 103 cmd_netdump = (cmd_netdump or 104 path_utils.get_install_path('tcpdump', host=host)) 105 host_description = host_description or 'cap_%s' % uuid.uuid4().hex 106 if None in [cmd_iw, cmd_ip, cmd_netdump, host_description, logdir]: 107 if ignore_failures: 108 logging.warning('Creating a disabled packet capturer for %s.', 109 host_description) 110 return DisabledPacketCapturer() 111 else: 112 raise error.TestFail('Missing commands needed for ' 113 'capturing packets') 114 115 return PacketCapturer(host, host_description, cmd_ip, cmd_iw, cmd_netdump, 116 logdir=logdir) 117 118 119class DisabledPacketCapturer(object): 120 """Delegate meant to look like it could take packet captures.""" 121 122 @property 123 def capture_running(self): 124 """@return False""" 125 return False 126 127 128 def __init__(self): 129 pass 130 131 132 def __enter__(self): 133 return self 134 135 136 def __exit__(self): 137 pass 138 139 140 def close(self): 141 """No-op""" 142 143 144 def create_raw_monitor(self, phy, frequency, width_type=None, 145 monitor_device=None): 146 """Appears to fail while creating a raw monitor device. 147 148 @param phy string ignored. 149 @param frequency int ignored. 150 @param width_type string ignored. 151 @param monitor_device string ignored. 152 @return None. 153 154 """ 155 return None 156 157 158 def configure_raw_monitor(self, monitor_device, frequency, width_type=None): 159 """Fails to configure a raw monitor. 160 161 @param monitor_device string ignored. 162 @param frequency int ignored. 163 @param width_type string ignored. 164 165 """ 166 167 168 def create_managed_monitor(self, existing_dev, monitor_device=None): 169 """Fails to create a managed monitor device. 170 171 @param existing_device string ignored. 172 @param monitor_device string ignored. 173 @return None 174 175 """ 176 return None 177 178 179 def start_capture(self, interface, local_save_dir, 180 remote_file=None, snaplen=None): 181 """Fails to start a packet capture. 182 183 @param interface string ignored. 184 @param local_save_dir string ignored. 185 @param remote_file string ignored. 186 @param snaplen int ignored. 187 188 @raises PacketCapturesDisabledError. 189 190 """ 191 raise PacketCapturesDisabledError() 192 193 194 def stop_capture(self, capture_pid=None): 195 """Stops all ongoing packet captures. 196 197 @param capture_pid int ignored. 198 199 """ 200 201 202class PacketCapturer(object): 203 """Delegate with capability to initiate packet captures on a remote host.""" 204 205 LIBPCAP_POLL_FREQ_SECS = 1 206 207 @property 208 def capture_running(self): 209 """@return True iff we have at least one ongoing packet capture.""" 210 if self._ongoing_captures: 211 return True 212 213 return False 214 215 216 def __init__(self, host, host_description, cmd_ip, cmd_iw, cmd_netdump, 217 logdir, disable_captures=False): 218 self._cmd_netdump = cmd_netdump 219 self._cmd_iw = cmd_iw 220 self._cmd_ip = cmd_ip 221 self._host = host 222 self._ongoing_captures = {} 223 self._cap_num = 0 224 self._if_num = 0 225 self._created_managed_devices = [] 226 self._created_raw_devices = [] 227 self._host_description = host_description 228 self._logdir = logdir 229 230 231 def __enter__(self): 232 return self 233 234 235 def __exit__(self): 236 self.close() 237 238 239 def close(self): 240 """Stop ongoing captures and destroy all created devices.""" 241 self.stop_capture() 242 for device in self._created_managed_devices: 243 self._host.run("%s dev %s del" % (self._cmd_iw, device)) 244 self._created_managed_devices = [] 245 for device in self._created_raw_devices: 246 self._host.run("%s link set %s down" % (self._cmd_ip, device)) 247 self._host.run("%s dev %s del" % (self._cmd_iw, device)) 248 self._created_raw_devices = [] 249 250 251 def create_raw_monitor(self, phy, frequency, width_type=None, 252 monitor_device=None): 253 """Create and configure a monitor type WiFi interface on a phy. 254 255 If a device called |monitor_device| already exists, it is first removed. 256 257 @param phy string phy name for created monitor (e.g. phy0). 258 @param frequency int frequency for created monitor to watch. 259 @param width_type object optional HT or VHT type, one of the keys in 260 self.WIDTH_STRINGS. 261 @param monitor_device string name of monitor interface to create. 262 @return string monitor device name created or None on failure. 263 264 """ 265 if not monitor_device: 266 monitor_device = 'mon%d' % self._if_num 267 self._if_num += 1 268 269 self._host.run('%s dev %s del' % (self._cmd_iw, monitor_device), 270 ignore_status=True) 271 result = self._host.run('%s phy %s interface add %s type monitor' % 272 (self._cmd_iw, 273 phy, 274 monitor_device), 275 ignore_status=True) 276 if result.exit_status: 277 logging.error('Failed creating raw monitor.') 278 return None 279 280 self.configure_raw_monitor(monitor_device, frequency, width_type) 281 self._created_raw_devices.append(monitor_device) 282 return monitor_device 283 284 285 def configure_raw_monitor(self, monitor_device, frequency, width_type=None): 286 """Configure a raw monitor with frequency and HT params. 287 288 Note that this will stomp on earlier device settings. 289 290 @param monitor_device string name of device to configure. 291 @param frequency int WiFi frequency to dwell on. 292 @param width_type object width_type, one of the WIDTH_* objects. 293 294 """ 295 channel_args = str(frequency) 296 297 if width_type: 298 width_string = _get_width_string(width_type) 299 if not width_string: 300 raise error.TestError('Invalid width type: %r' % width_type) 301 if width_type == WIDTH_VHT80_80: 302 raise error.TestError('VHT80+80 packet capture not supported') 303 if width_type == WIDTH_VHT80: 304 width_string = '%s %d' % (width_string, 305 _get_center_freq_80(frequency)) 306 elif width_type == WIDTH_VHT160: 307 width_string = '%s %d' % (width_string, 308 _get_center_freq_160(frequency)) 309 channel_args = '%s %s' % (channel_args, width_string) 310 311 self._host.run("%s link set %s up" % (self._cmd_ip, monitor_device)) 312 self._host.run("%s dev %s set freq %s" % (self._cmd_iw, 313 monitor_device, 314 channel_args)) 315 316 317 def create_managed_monitor(self, existing_dev, monitor_device=None): 318 """Create a monitor type WiFi interface next to a managed interface. 319 320 If a device called |monitor_device| already exists, it is first removed. 321 322 @param existing_device string existing interface (e.g. mlan0). 323 @param monitor_device string name of monitor interface to create. 324 @return string monitor device name created or None on failure. 325 326 """ 327 if not monitor_device: 328 monitor_device = 'mon%d' % self._if_num 329 self._if_num += 1 330 self._host.run('%s dev %s del' % (self._cmd_iw, monitor_device), 331 ignore_status=True) 332 result = self._host.run('%s dev %s interface add %s type monitor' % 333 (self._cmd_iw, 334 existing_dev, 335 monitor_device), 336 ignore_status=True) 337 if result.exit_status: 338 logging.warning('Failed creating monitor.') 339 return None 340 341 self._host.run('%s link set %s up' % (self._cmd_ip, monitor_device)) 342 self._created_managed_devices.append(monitor_device) 343 return monitor_device 344 345 346 def _is_capture_active(self, remote_log_file): 347 """Check if a packet capture has completed initialization. 348 349 @param remote_log_file string path to the capture's log file 350 @return True iff log file indicates that tcpdump is listening. 351 """ 352 return self._host.run( 353 'grep "listening on" "%s"' % remote_log_file, ignore_status=True 354 ).exit_status == 0 355 356 357 def start_capture(self, interface, local_save_dir, 358 remote_file=None, snaplen=None): 359 """Start a packet capture on an existing interface. 360 361 @param interface string existing interface to capture on. 362 @param local_save_dir string directory on local machine to hold results. 363 @param remote_file string full path on remote host to hold the capture. 364 @param snaplen int maximum captured frame length. 365 @return int pid of started packet capture. 366 367 """ 368 remote_file = (remote_file or 369 '%s/%s.%d.pcap' % (self._logdir, self._host_description, 370 self._cap_num)) 371 self._cap_num += 1 372 remote_log_file = '%s.log' % remote_file 373 # Redirect output because SSH refuses to return until the child file 374 # descriptors are closed. 375 cmd = '%s -U -i %s -w %s -s %d >%s 2>&1 & echo $!' % ( 376 self._cmd_netdump, 377 interface, 378 remote_file, 379 snaplen or 0, 380 remote_log_file) 381 logging.debug('Starting managed packet capture') 382 pid = int(self._host.run(cmd).stdout) 383 self._ongoing_captures[pid] = (remote_file, 384 remote_log_file, 385 local_save_dir) 386 is_capture_active = lambda: self._is_capture_active(remote_log_file) 387 utils.poll_for_condition( 388 is_capture_active, 389 timeout=TCPDUMP_START_TIMEOUT_SECONDS, 390 sleep_interval=TCPDUMP_START_POLL_SECONDS, 391 desc='Timeout waiting for tcpdump to start.') 392 return pid 393 394 395 def stop_capture(self, capture_pid=None, local_save_dir=None, 396 local_pcap_filename=None): 397 """Stop an ongoing packet capture, or all ongoing packet captures. 398 399 If |capture_pid| is given, stops that capture, otherwise stops all 400 ongoing captures. 401 402 This method may sleep for a small amount of time, to ensure that 403 libpcap has completed its last poll(). The caller must ensure that 404 no unwanted traffic is received during this time. 405 406 @param capture_pid int pid of ongoing packet capture or None. 407 @param local_save_dir path to directory to save pcap file in locally. 408 @param local_pcap_filename name of file to store pcap in 409 (basename only). 410 @return list of RemoteCaptureResult tuples 411 412 """ 413 if capture_pid: 414 pids_to_kill = [capture_pid] 415 else: 416 pids_to_kill = list(self._ongoing_captures.keys()) 417 418 if pids_to_kill: 419 time.sleep(self.LIBPCAP_POLL_FREQ_SECS * 2) 420 421 results = [] 422 for pid in pids_to_kill: 423 self._host.run('kill -INT %d' % pid, ignore_status=True) 424 remote_pcap, remote_pcap_log, save_dir = self._ongoing_captures[pid] 425 pcap_filename = os.path.basename(remote_pcap) 426 pcap_log_filename = os.path.basename(remote_pcap_log) 427 if local_pcap_filename: 428 pcap_filename = os.path.join(local_save_dir or save_dir, 429 local_pcap_filename) 430 pcap_log_filename = os.path.join(local_save_dir or save_dir, 431 '%s.log' % local_pcap_filename) 432 pairs = [(remote_pcap, pcap_filename), 433 (remote_pcap_log, pcap_log_filename)] 434 435 for remote_file, local_file in pairs: 436 self._host.get_file(remote_file, local_file) 437 self._host.run('rm -f %s' % remote_file) 438 439 self._ongoing_captures.pop(pid) 440 results.append(CaptureResult(pcap_filename, 441 pcap_log_filename)) 442 return results 443