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