blob: eda42be6b1e9bf752d9263d448c1904286261d6c [file] [log] [blame]
# Copyright (c) 2013 The Chromium OS Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import collections
import logging
import os.path
import time
import uuid
from autotest_lib.client.bin import utils
from autotest_lib.client.common_lib import error
from autotest_lib.client.common_lib.cros import path_utils
from autotest_lib.client.common_lib.cros.network import iw_runner
class PacketCapturesDisabledError(Exception):
"""Signifies that this remote host does not support packet captures."""
pass
# local_pcap_path refers to the path of the result on the local host.
# local_log_path refers to the tcpdump log file path on the local host.
CaptureResult = collections.namedtuple('CaptureResult',
['local_pcap_path', 'local_log_path'])
# The number of bytes needed for a probe request is hard to define,
# because the frame contents are variable (e.g. radiotap header may
# contain different fields, maybe SSID isn't the first tagged
# parameter?). The value here is 2x the largest frame size observed in
# a quick sample.
SNAPLEN_WIFI_PROBE_REQUEST = 600
TCPDUMP_START_TIMEOUT_SECONDS = 5
TCPDUMP_START_POLL_SECONDS = 0.1
# These are WidthType objects from iw_runner
WIDTH_HT20 = iw_runner.WIDTH_HT20
WIDTH_HT40_PLUS = iw_runner.WIDTH_HT40_PLUS
WIDTH_HT40_MINUS = iw_runner.WIDTH_HT40_MINUS
WIDTH_VHT80 = iw_runner.WIDTH_VHT80
WIDTH_VHT160 = iw_runner.WIDTH_VHT160
WIDTH_VHT80_80 = iw_runner.WIDTH_VHT80_80
_WIDTH_STRINGS = {
WIDTH_HT20: 'HT20',
WIDTH_HT40_PLUS: 'HT40+',
WIDTH_HT40_MINUS: 'HT40-',
WIDTH_VHT80: '80',
WIDTH_VHT160: '160',
WIDTH_VHT80_80: '80+80',
}
def _get_width_string(width):
"""Returns a valid width parameter for "iw dev ${DEV} set freq".
@param width object, one of WIDTH_*
@return string iw readable width, or empty string
"""
return _WIDTH_STRINGS.get(width, '')
def _get_center_freq_80(frequency):
"""Find the center frequency of a 80MHz channel.
Raises an error upon an invalid frequency.
@param frequency int Control frequency of the channel.
@return center_freq int Center frequency of the channel.
"""
vht80 = [ 5180, 5260, 5500, 5580, 5660, 5745 ]
for f in vht80:
if frequency >= f and frequency < f + 80:
return f + 30
raise error.TestError(
'Frequency %s is not part of a 80MHz channel', frequency)
def _get_center_freq_160(frequency):
"""Find the center frequency of a 160MHz channel.
Raises an error upon an invalid frequency.
@param frequency int Control frequency of the channel.
@return center_freq int Center frequency of the channel.
"""
if (frequency >= 5180 and frequency <= 5320):
return 5250
if (frequency >= 5500 and frequency <= 5640):
return 5570
raise error.TestError(
'Frequency %s is not part of a 160MHz channel', frequency)
def get_packet_capturer(host, host_description=None, cmd_ip=None, cmd_iw=None,
cmd_netdump=None, ignore_failures=False, logdir=None):
cmd_iw = cmd_iw or path_utils.get_install_path('iw', host=host)
cmd_ip = cmd_ip or path_utils.get_install_path('ip', host=host)
cmd_netdump = (cmd_netdump or
path_utils.get_install_path('tcpdump', host=host))
host_description = host_description or 'cap_%s' % uuid.uuid4().hex
if None in [cmd_iw, cmd_ip, cmd_netdump, host_description, logdir]:
if ignore_failures:
logging.warning('Creating a disabled packet capturer for %s.',
host_description)
return DisabledPacketCapturer()
else:
raise error.TestFail('Missing commands needed for '
'capturing packets')
return PacketCapturer(host, host_description, cmd_ip, cmd_iw, cmd_netdump,
logdir=logdir)
class DisabledPacketCapturer(object):
"""Delegate meant to look like it could take packet captures."""
@property
def capture_running(self):
"""@return False"""
return False
def __init__(self):
pass
def __enter__(self):
return self
def __exit__(self):
pass
def close(self):
"""No-op"""
def create_raw_monitor(self, phy, frequency, width_type=None,
monitor_device=None):
"""Appears to fail while creating a raw monitor device.
@param phy string ignored.
@param frequency int ignored.
@param width_type string ignored.
@param monitor_device string ignored.
@return None.
"""
return None
def configure_raw_monitor(self, monitor_device, frequency, width_type=None):
"""Fails to configure a raw monitor.
@param monitor_device string ignored.
@param frequency int ignored.
@param width_type string ignored.
"""
def create_managed_monitor(self, existing_dev, monitor_device=None):
"""Fails to create a managed monitor device.
@param existing_device string ignored.
@param monitor_device string ignored.
@return None
"""
return None
def start_capture(self, interface, local_save_dir,
remote_file=None, snaplen=None):
"""Fails to start a packet capture.
@param interface string ignored.
@param local_save_dir string ignored.
@param remote_file string ignored.
@param snaplen int ignored.
@raises PacketCapturesDisabledError.
"""
raise PacketCapturesDisabledError()
def stop_capture(self, capture_pid=None):
"""Stops all ongoing packet captures.
@param capture_pid int ignored.
"""
class PacketCapturer(object):
"""Delegate with capability to initiate packet captures on a remote host."""
LIBPCAP_POLL_FREQ_SECS = 1
@property
def capture_running(self):
"""@return True iff we have at least one ongoing packet capture."""
if self._ongoing_captures:
return True
return False
def __init__(self, host, host_description, cmd_ip, cmd_iw, cmd_netdump,
logdir, disable_captures=False):
self._cmd_netdump = cmd_netdump
self._cmd_iw = cmd_iw
self._cmd_ip = cmd_ip
self._host = host
self._ongoing_captures = {}
self._cap_num = 0
self._if_num = 0
self._created_managed_devices = []
self._created_raw_devices = []
self._host_description = host_description
self._logdir = logdir
def __enter__(self):
return self
def __exit__(self):
self.close()
def close(self):
"""Stop ongoing captures and destroy all created devices."""
self.stop_capture()
for device in self._created_managed_devices:
self._host.run("%s dev %s del" % (self._cmd_iw, device))
self._created_managed_devices = []
for device in self._created_raw_devices:
self._host.run("%s link set %s down" % (self._cmd_ip, device))
self._host.run("%s dev %s del" % (self._cmd_iw, device))
self._created_raw_devices = []
def create_raw_monitor(self, phy, frequency, width_type=None,
monitor_device=None):
"""Create and configure a monitor type WiFi interface on a phy.
If a device called |monitor_device| already exists, it is first removed.
@param phy string phy name for created monitor (e.g. phy0).
@param frequency int frequency for created monitor to watch.
@param width_type object optional HT or VHT type, one of the keys in
self.WIDTH_STRINGS.
@param monitor_device string name of monitor interface to create.
@return string monitor device name created or None on failure.
"""
if not monitor_device:
monitor_device = 'mon%d' % self._if_num
self._if_num += 1
self._host.run('%s dev %s del' % (self._cmd_iw, monitor_device),
ignore_status=True)
result = self._host.run('%s phy %s interface add %s type monitor' %
(self._cmd_iw,
phy,
monitor_device),
ignore_status=True)
if result.exit_status:
logging.error('Failed creating raw monitor.')
return None
self.configure_raw_monitor(monitor_device, frequency, width_type)
self._created_raw_devices.append(monitor_device)
return monitor_device
def configure_raw_monitor(self, monitor_device, frequency, width_type=None):
"""Configure a raw monitor with frequency and HT params.
Note that this will stomp on earlier device settings.
@param monitor_device string name of device to configure.
@param frequency int WiFi frequency to dwell on.
@param width_type object width_type, one of the WIDTH_* objects.
"""
channel_args = str(frequency)
if width_type:
width_string = _get_width_string(width_type)
if not width_string:
raise error.TestError('Invalid width type: %r' % width_type)
if width_type == WIDTH_VHT80_80:
raise error.TestError('VHT80+80 packet capture not supported')
if width_type == WIDTH_VHT80:
width_string = '%s %d' % (width_string,
_get_center_freq_80(frequency))
elif width_type == WIDTH_VHT160:
width_string = '%s %d' % (width_string,
_get_center_freq_160(frequency))
channel_args = '%s %s' % (channel_args, width_string)
self._host.run("%s link set %s up" % (self._cmd_ip, monitor_device))
self._host.run("%s dev %s set freq %s" % (self._cmd_iw,
monitor_device,
channel_args))
def create_managed_monitor(self, existing_dev, monitor_device=None):
"""Create a monitor type WiFi interface next to a managed interface.
If a device called |monitor_device| already exists, it is first removed.
@param existing_device string existing interface (e.g. mlan0).
@param monitor_device string name of monitor interface to create.
@return string monitor device name created or None on failure.
"""
if not monitor_device:
monitor_device = 'mon%d' % self._if_num
self._if_num += 1
self._host.run('%s dev %s del' % (self._cmd_iw, monitor_device),
ignore_status=True)
result = self._host.run('%s dev %s interface add %s type monitor' %
(self._cmd_iw,
existing_dev,
monitor_device),
ignore_status=True)
if result.exit_status:
logging.warning('Failed creating monitor.')
return None
self._host.run('%s link set %s up' % (self._cmd_ip, monitor_device))
self._created_managed_devices.append(monitor_device)
return monitor_device
def _is_capture_active(self, remote_log_file):
"""Check if a packet capture has completed initialization.
@param remote_log_file string path to the capture's log file
@return True iff log file indicates that tcpdump is listening.
"""
return self._host.run(
'grep "listening on" "%s"' % remote_log_file, ignore_status=True
).exit_status == 0
def start_capture(self, interface, local_save_dir,
remote_file=None, snaplen=None):
"""Start a packet capture on an existing interface.
@param interface string existing interface to capture on.
@param local_save_dir string directory on local machine to hold results.
@param remote_file string full path on remote host to hold the capture.
@param snaplen int maximum captured frame length.
@return int pid of started packet capture.
"""
remote_file = (remote_file or
'%s/%s.%d.pcap' % (self._logdir, self._host_description,
self._cap_num))
self._cap_num += 1
remote_log_file = '%s.log' % remote_file
# Redirect output because SSH refuses to return until the child file
# descriptors are closed.
cmd = '%s -U -i %s -w %s -s %d >%s 2>&1 & echo $!' % (
self._cmd_netdump,
interface,
remote_file,
snaplen or 0,
remote_log_file)
logging.debug('Starting managed packet capture')
pid = int(self._host.run(cmd).stdout)
self._ongoing_captures[pid] = (remote_file,
remote_log_file,
local_save_dir)
is_capture_active = lambda: self._is_capture_active(remote_log_file)
utils.poll_for_condition(
is_capture_active,
timeout=TCPDUMP_START_TIMEOUT_SECONDS,
sleep_interval=TCPDUMP_START_POLL_SECONDS,
desc='Timeout waiting for tcpdump to start.')
return pid
def stop_capture(self, capture_pid=None, local_save_dir=None,
local_pcap_filename=None):
"""Stop an ongoing packet capture, or all ongoing packet captures.
If |capture_pid| is given, stops that capture, otherwise stops all
ongoing captures.
This method may sleep for a small amount of time, to ensure that
libpcap has completed its last poll(). The caller must ensure that
no unwanted traffic is received during this time.
@param capture_pid int pid of ongoing packet capture or None.
@param local_save_dir path to directory to save pcap file in locally.
@param local_pcap_filename name of file to store pcap in
(basename only).
@return list of RemoteCaptureResult tuples
"""
if capture_pid:
pids_to_kill = [capture_pid]
else:
pids_to_kill = list(self._ongoing_captures.keys())
if pids_to_kill:
time.sleep(self.LIBPCAP_POLL_FREQ_SECS * 2)
results = []
for pid in pids_to_kill:
self._host.run('kill -INT %d' % pid, ignore_status=True)
remote_pcap, remote_pcap_log, save_dir = self._ongoing_captures[pid]
pcap_filename = os.path.basename(remote_pcap)
pcap_log_filename = os.path.basename(remote_pcap_log)
if local_pcap_filename:
pcap_filename = os.path.join(local_save_dir or save_dir,
local_pcap_filename)
pcap_log_filename = os.path.join(local_save_dir or save_dir,
'%s.log' % local_pcap_filename)
pairs = [(remote_pcap, pcap_filename),
(remote_pcap_log, pcap_log_filename)]
for remote_file, local_file in pairs:
self._host.get_file(remote_file, local_file)
self._host.run('rm -f %s' % remote_file)
self._ongoing_captures.pop(pid)
results.append(CaptureResult(pcap_filename,
pcap_log_filename))
return results