blob: b639a4c9df888ff2cc1d65e03b366fabf3e1b60d [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
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
def get_packet_capturer(host, host_description=None, cmd_ifconfig=None,
cmd_ip=None, cmd_iw=None, cmd_netdump=None,
ignore_failures=False):
cmd_ifconfig = (cmd_ifconfig or
path_utils.get_install_path('ifconfig', host=host))
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_ifconfig, cmd_iw, cmd_ip, cmd_netdump, host_description]:
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_ifconfig, cmd_ip, cmd_iw,
cmd_netdump)
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, ht_type=None,
monitor_device=None):
"""Appears to fail while creating a raw monitor device.
@param phy string ignored.
@param frequency int ignored.
@param ht_type string ignored.
@param monitor_device string ignored.
@return None.
"""
return None
def configure_raw_monitor(self, monitor_device, frequency, ht_type=None):
"""Fails to configure a raw monitor.
@param monitor_device string ignored.
@param frequency int ignored.
@param ht_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_ifconfig, cmd_ip,
cmd_iw, cmd_netdump, disable_captures=False):
self._cmd_netdump = cmd_netdump
self._cmd_iw = cmd_iw
self._cmd_ip = cmd_ip
self._cmd_ifconfig = cmd_ifconfig
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
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, ht_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 ht_type string optional HT type ('HT20', 'HT40+', or 'HT40-').
@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, ht_type)
self._created_raw_devices.append(monitor_device)
return monitor_device
def configure_raw_monitor(self, monitor_device, frequency, ht_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 ht_type string optional HT type ('HT20', 'HT40+', or 'HT40-').
"""
channel_args = str(frequency)
if ht_type:
ht_type = ht_type.upper()
channel_args = '%s %s' % (channel_args, ht_type)
if ht_type not in ('HT20', 'HT40+', 'HT40-'):
raise error.TestError('Cannot set HT mode: %s', ht_type)
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 %s up' % (self._cmd_ifconfig, 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
'/tmp/%s.%d.pcap' % (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 will 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
"""
time.sleep(self.LIBPCAP_POLL_FREQ_SECS * 2)
if capture_pid:
pids_to_kill = [capture_pid]
else:
pids_to_kill = list(self._ongoing_captures.keys())
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