You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
442 lines
15 KiB
442 lines
15 KiB
# 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
|