• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#!/usr/bin/env python3
2#
3#   Copyright 2018 - Google, Inc.
4#
5#   Licensed under the Apache License, Version 2.0 (the "License");
6#   you may not use this file except in compliance with the License.
7#   You may obtain a copy of the License at
8#
9#       http://www.apache.org/licenses/LICENSE-2.0
10#
11#   Unless required by applicable law or agreed to in writing, software
12#   distributed under the License is distributed on an "AS IS" BASIS,
13#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14#   See the License for the specific language governing permissions and
15#   limitations under the License.
16
17from acts import logger
18from acts.controllers.ap_lib.hostapd_constants import AP_DEFAULT_CHANNEL_2G
19from acts.controllers.ap_lib.hostapd_constants import AP_DEFAULT_CHANNEL_5G
20from acts.controllers.ap_lib.hostapd_constants import CHANNEL_MAP
21from acts.controllers.ap_lib.hostapd_constants import FREQUENCY_MAP
22from acts.controllers.ap_lib.hostapd_constants import CENTER_CHANNEL_MAP
23from acts.controllers.ap_lib.hostapd_constants import VHT_CHANNEL
24from acts.controllers.utils_lib.ssh import connection
25from acts.controllers.utils_lib.ssh import formatter
26from acts.controllers.utils_lib.ssh import settings
27from acts.libs.logging import log_stream
28from acts.libs.proc.process import Process
29from acts import asserts
30
31import logging
32import os
33import threading
34import time
35
36ACTS_CONTROLLER_CONFIG_NAME = 'PacketCapture'
37ACTS_CONTROLLER_REFERENCE_NAME = 'packet_capture'
38BSS = 'BSS'
39BSSID = 'BSSID'
40FREQ = 'freq'
41FREQUENCY = 'frequency'
42LEVEL = 'level'
43MON_2G = 'mon0'
44MON_5G = 'mon1'
45BAND_IFACE = {'2G': MON_2G, '5G': MON_5G}
46SCAN_IFACE = 'wlan2'
47SCAN_TIMEOUT = 60
48SEP = ':'
49SIGNAL = 'signal'
50SSID = 'SSID'
51
52
53def create(configs):
54    return [PacketCapture(c) for c in configs]
55
56
57def destroy(pcaps):
58    for pcap in pcaps:
59        pcap.close()
60
61
62def get_info(pcaps):
63    return [pcap.ssh_settings.hostname for pcap in pcaps]
64
65
66class PcapProperties(object):
67    """Class to maintain packet capture properties after starting tcpdump.
68
69    Attributes:
70        proc: Process object of tcpdump
71        pcap_fname: File name of the tcpdump output file
72        pcap_file: File object for the tcpdump output file
73    """
74    def __init__(self, proc, pcap_fname, pcap_file):
75        """Initialize object."""
76        self.proc = proc
77        self.pcap_fname = pcap_fname
78        self.pcap_file = pcap_file
79
80
81class PacketCaptureError(Exception):
82    """Error related to Packet capture."""
83
84
85class PacketCapture(object):
86    """Class representing packet capturer.
87
88    An instance of this class creates and configures two interfaces for monitor
89    mode; 'mon0' for 2G and 'mon1' for 5G and one interface for scanning for
90    wifi networks; 'wlan2' which is a dual band interface.
91
92    Attributes:
93        pcap_properties: dict that specifies packet capture properties for a
94            band.
95    """
96    def __init__(self, configs):
97        """Initialize objects.
98
99        Args:
100            configs: config for the packet capture.
101        """
102        self.ssh_settings = settings.from_config(configs['ssh_config'])
103        self.ssh = connection.SshConnection(self.ssh_settings)
104        self.log = logger.create_logger(lambda msg: '[%s|%s] %s' % (
105            ACTS_CONTROLLER_CONFIG_NAME, self.ssh_settings.hostname, msg))
106
107        self._create_interface(MON_2G, 'monitor')
108        self._create_interface(MON_5G, 'monitor')
109        self._create_interface(SCAN_IFACE, 'managed')
110
111        self.pcap_properties = dict()
112        self._pcap_stop_lock = threading.Lock()
113
114    def _create_interface(self, iface, mode):
115        """Create interface of monitor/managed mode.
116
117        Create mon0/mon1 for 2G/5G monitor mode and wlan2 for managed mode.
118        """
119        self.ssh.run('iw dev %s del' % iface, ignore_status=True)
120        self.ssh.run('iw phy%s interface add %s type %s'
121                     % (iface[-1], iface, mode), ignore_status=True)
122        self.ssh.run('ip link set %s up' % iface, ignore_status=True)
123        result = self.ssh.run('iw dev %s info' % iface, ignore_status=True)
124        if result.stderr or iface not in result.stdout:
125            raise PacketCaptureError('Failed to configure interface %s' % iface)
126
127    def _cleanup_interface(self, iface):
128        """Clean up monitor mode interfaces."""
129        self.ssh.run('iw dev %s del' % iface, ignore_status=True)
130        result = self.ssh.run('iw dev %s info' % iface, ignore_status=True)
131        if not result.stderr or 'No such device' not in result.stderr:
132            raise PacketCaptureError('Failed to cleanup monitor mode for %s'
133                                     % iface)
134
135    def _parse_scan_results(self, scan_result):
136        """Parses the scan dump output and returns list of dictionaries.
137
138        Args:
139            scan_result: scan dump output from scan on mon interface.
140
141        Returns:
142            Dictionary of found network in the scan.
143            The attributes returned are
144                a.) SSID - SSID of the network.
145                b.) LEVEL - signal level.
146                c.) FREQUENCY - WiFi band the network is on.
147                d.) BSSID - BSSID of the network.
148        """
149        scan_networks = []
150        network = {}
151        for line in scan_result.splitlines():
152            if SEP not in line:
153                continue
154            if BSS in line:
155                network[BSSID] = line.split('(')[0].split()[-1]
156            field, value = line.lstrip().rstrip().split(SEP)[0:2]
157            value = value.lstrip()
158            if SIGNAL in line:
159                network[LEVEL] = int(float(value.split()[0]))
160            elif FREQ in line:
161                network[FREQUENCY] = int(value)
162            elif SSID in line:
163                network[SSID] = value
164                scan_networks.append(network)
165                network = {}
166        return scan_networks
167
168    def get_wifi_scan_results(self):
169        """Starts a wifi scan on wlan2 interface.
170
171        Returns:
172            List of dictionaries each representing a found network.
173        """
174        result = self.ssh.run('iw dev %s scan' % SCAN_IFACE)
175        if result.stderr:
176            raise PacketCaptureError('Failed to get scan dump')
177        if not result.stdout:
178            return []
179        return self._parse_scan_results(result.stdout)
180
181    def start_scan_and_find_network(self, ssid):
182        """Start a wifi scan on wlan2 interface and find network.
183
184        Args:
185            ssid: SSID of the network.
186
187        Returns:
188            True/False if the network if found or not.
189        """
190        curr_time = time.time()
191        while time.time() < curr_time + SCAN_TIMEOUT:
192            found_networks = self.get_wifi_scan_results()
193            for network in found_networks:
194                if network[SSID] == ssid:
195                    return True
196            time.sleep(3)  # sleep before next scan
197        return False
198
199    def configure_monitor_mode(self, band, channel, bandwidth=20):
200        """Configure monitor mode.
201
202        Args:
203            band: band to configure monitor mode for.
204            channel: channel to set for the interface.
205            bandwidth : bandwidth for VHT channel as 40,80,160
206
207        Returns:
208            True if configure successful.
209            False if not successful.
210        """
211
212        band = band.upper()
213        if band not in BAND_IFACE:
214            self.log.error('Invalid band. Must be 2g/2G or 5g/5G')
215            return False
216
217        iface = BAND_IFACE[band]
218        if bandwidth == 20:
219            self.ssh.run('iw dev %s set channel %s' %
220                         (iface, channel), ignore_status=True)
221        else:
222            center_freq = None
223            for i, j in CENTER_CHANNEL_MAP[VHT_CHANNEL[bandwidth]]["channels"]:
224                if channel in range(i, j + 1):
225                    center_freq = (FREQUENCY_MAP[i] + FREQUENCY_MAP[j]) / 2
226                    break
227            asserts.assert_true(center_freq,
228                                "No match channel in VHT channel list.")
229            self.ssh.run('iw dev %s set freq %s %s %s' %
230                (iface, FREQUENCY_MAP[channel],
231                bandwidth, center_freq), ignore_status=True)
232
233        result = self.ssh.run('iw dev %s info' % iface, ignore_status=True)
234        if result.stderr or 'channel %s' % channel not in result.stdout:
235            self.log.error("Failed to configure monitor mode for %s" % band)
236            return False
237        return True
238
239    def start_packet_capture(self, band, log_path, pcap_fname):
240        """Start packet capture for band.
241
242        band = 2G starts tcpdump on 'mon0' interface.
243        band = 5G starts tcpdump on 'mon1' interface.
244
245        Args:
246            band: '2g' or '2G' and '5g' or '5G'.
247            log_path: test log path to save the pcap file.
248            pcap_fname: name of the pcap file.
249
250        Returns:
251            pcap_proc: Process object of the tcpdump.
252        """
253        band = band.upper()
254        if band not in BAND_IFACE.keys() or band in self.pcap_properties:
255            self.log.error("Invalid band or packet capture already running")
256            return None
257
258        pcap_name = '%s_%s.pcap' % (pcap_fname, band)
259        pcap_fname = os.path.join(log_path, pcap_name)
260        pcap_file = open(pcap_fname, 'w+b')
261
262        tcpdump_cmd = 'tcpdump -i %s -w - -U 2>/dev/null' % (BAND_IFACE[band])
263        cmd = formatter.SshFormatter().format_command(
264            tcpdump_cmd, None, self.ssh_settings, extra_flags={'-q': None})
265        pcap_proc = Process(cmd)
266        pcap_proc.set_on_output_callback(
267            lambda msg: pcap_file.write(msg), binary=True)
268        pcap_proc.start()
269
270        self.pcap_properties[band] = PcapProperties(pcap_proc, pcap_fname,
271                                                    pcap_file)
272        return pcap_proc
273
274    def stop_packet_capture(self, proc):
275        """Stop the packet capture.
276
277        Args:
278            proc: Process object of tcpdump to kill.
279        """
280        for key, val in self.pcap_properties.items():
281            if val.proc is proc:
282                break
283        else:
284            self.log.error("Failed to stop tcpdump. Invalid process.")
285            return
286
287        proc.stop()
288        with self._pcap_stop_lock:
289            self.pcap_properties[key].pcap_file.close()
290            del self.pcap_properties[key]
291
292    def close(self):
293        """Cleanup.
294
295        Cleans up all the monitor mode interfaces and closes ssh connections.
296        """
297        self._cleanup_interface(MON_2G)
298        self._cleanup_interface(MON_5G)
299        self.ssh.close()
300