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