• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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