1 # Copyright (c) 2015 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 5 import logging 6 import subprocess 7 import tempfile 8 9 from autotest_lib.client.bin import utils 10 from autotest_lib.client.common_lib import error 11 from autotest_lib.client.common_lib.cros.network import iw_runner 12 from autotest_lib.client.common_lib.cros.network import xmlrpc_datatypes 13 from autotest_lib.server import test 14 from autotest_lib.server.cros.network import hostap_config 15 from autotest_lib.server.cros.network import wifi_test_context_manager 16 17 18 class network_WiFi_RegDomain(test.test): 19 """Verifies that a DUT connects, or fails to connect, on particular 20 channels, in particular regions, per expectations.""" 21 version = 1 22 23 24 MISSING_SSID = "MissingSsid" 25 # TODO(quiche): Shrink or remove the repeat count, once we've 26 # figured out why tcpdump sometimes misses data. crbug.com/477536 27 PASSIVE_SCAN_REPEAT_COUNT = 30 28 REBOOT_TIMEOUT_SECS = 60 29 # TODO(quiche): Migrate to the shiny new pyshark code from rpius. 30 TSHARK_COMMAND = 'tshark' 31 TSHARK_DISABLE_NAME_RESOLUTION = '-n' 32 TSHARK_READ_FILE = '-r' 33 TSHARK_SRC_FILTER = 'wlan.sa == %s' 34 VPD_CACHE_FILE = \ 35 '/mnt/stateful_partition/unencrypted/cache/vpd/full-v2.txt' 36 VPD_CLEAN_COMMAND ='dump_vpd_log --clean' 37 38 39 @staticmethod 40 def assert_equal(description, actual, expected): 41 """Verifies that |actual| equals |expected|. 42 43 @param description A string describing the data being checked. 44 @param actual The actual value encountered by the test. 45 @param expected The value we expected to encounter. 46 @raise error.TestFail If actual != expected. 47 48 """ 49 if actual != expected: 50 raise error.TestFail( 51 'Expected %s |%s|, but got |%s|.' % 52 (description, expected, actual)) 53 54 55 @staticmethod 56 def phy_list_to_channel_expectations(phy_list): 57 """Maps phy information to expected scanning/connection behavior. 58 59 Converts phy information from iw_runner.IwRunner.list_phys() 60 into a map from channel numbers to expected connection 61 behavior. This mapping is useful for comparison with the 62 expectations programmed into the control file. 63 64 @param phy_list The return value of iw_runner.IwRunner.list_phys() 65 @return A dict from channel numbers to expected behaviors. 66 67 """ 68 channel_to_expectation = {} 69 for phy in phy_list: 70 for band in phy.bands: 71 for frequency, flags in band.frequency_flags.iteritems(): 72 channel = ( 73 hostap_config.HostapConfig.get_channel_for_frequency( 74 frequency)) 75 # While we don't expect a channel to have both 76 # CHAN_FLAG_DISABLED, and (CHAN_FLAG_PASSIVE_SCAN 77 # or CHAN_FLAG_NO_IR), we still test the most 78 # restrictive flag first. 79 if iw_runner.CHAN_FLAG_DISABLED in flags: 80 channel_to_expectation[channel] = 'no-connect' 81 elif (iw_runner.CHAN_FLAG_PASSIVE_SCAN in flags or 82 iw_runner.CHAN_FLAG_NO_IR in flags): 83 channel_to_expectation[channel] = 'passive-scan' 84 else: 85 channel_to_expectation[channel] = 'connect' 86 return channel_to_expectation 87 88 89 @staticmethod 90 def test_connect(wifi_context, frequency, expect_connect, hide_ssid): 91 """Verifies that a DUT does/does not connect on a particular frequency. 92 93 @param wifi_context: A WiFiTestContextManager. 94 @param frequency: int frequency to test. 95 @param expect_connect: bool whether or not connection should succeed. 96 @param hide_ssid: bool whether or not the AP should hide its SSID. 97 @raise error.TestFail if behavior does not match expectation. 98 99 """ 100 try: 101 router_ssid = None 102 if hide_ssid: 103 pcap_name = '%d_connect_hidden.pcap' % frequency 104 test_description = 'hidden' 105 else: 106 pcap_name = '%d_connect_visible.pcap' % frequency 107 test_description = 'visible' 108 wifi_context.capture_host.start_capture(frequency, 109 filename=pcap_name) 110 wifi_context.router.hostap_configure( 111 hostap_config.HostapConfig( 112 frequency=frequency, 113 hide_ssid=hide_ssid, 114 mode=hostap_config.HostapConfig.MODE_11N_MIXED)) 115 router_ssid = wifi_context.router.get_ssid() 116 client_conf = xmlrpc_datatypes.AssociationParameters( 117 ssid=router_ssid, 118 is_hidden=hide_ssid, 119 expect_failure=not expect_connect 120 ) 121 wifi_context.assert_connect_wifi(client_conf, test_description) 122 finally: 123 if router_ssid: 124 wifi_context.client.shill.delete_entries_for_ssid(router_ssid) 125 wifi_context.capture_host.stop_capture() 126 127 128 @classmethod 129 def count_mismatched_phy_configs(cls, dut_host, expected_channel_configs): 130 """Verifies that phys on the DUT place the expected restrictions on 131 channels. 132 133 Compares the restrictions reported by the running system to 134 the restrictions in |expected_channel_configs|. Returns a 135 count of the number of mismatches. 136 137 Note that this method deliberately ignores channels that are 138 reported by the running system, but not mentioned in 139 |expected_channel_configs|. This allows us to program the 140 control file with "spot checks", rather than an exhaustive 141 list of channels. 142 143 @param dut_host The host object for the DUT. 144 @param expected_channel_configs A channel_infos list. 145 @return int count of mismatches 146 147 """ 148 actual_channel_expectations = cls.phy_list_to_channel_expectations( 149 iw_runner.IwRunner(dut_host).list_phys()) 150 mismatches = 0 151 for expected_config in expected_channel_configs: 152 channel = expected_config['chnum'] 153 expected = expected_config['expect'] 154 actual = actual_channel_expectations[channel] 155 if actual != expected: 156 logging.error( 157 'Expected phy config for channel %d of |%s|, but got |%s|.', 158 channel, expected, actual) 159 mismatches += 1 160 return mismatches 161 162 163 @classmethod 164 def assert_scanning_is_passive(cls, client, capturer, scan_freq): 165 """Initiates single-channel scans, and verifies no probes are sent. 166 167 @param client The WiFiClient object for the DUT. 168 @param capturer The LinuxSystem object for the router or pcap_host. 169 @param scan_freq The frequency (in MHz) on which to scan. 170 """ 171 try: 172 client.claim_wifi_if() # Stop shill/supplicant scans. 173 capturer.start_capture( 174 scan_freq, filename='%d_scan.pcap' % scan_freq) 175 for _ in range(0, cls.PASSIVE_SCAN_REPEAT_COUNT): 176 # We pass in an SSID here, to check that even hidden 177 # SSIDs do not cause probe requests to be sent. 178 client.scan( 179 [scan_freq], [cls.MISSING_SSID], require_match=False) 180 pcap_path = capturer.stop_capture()[0].local_pcap_path 181 dut_frames = subprocess.check_output( 182 [cls.TSHARK_COMMAND, 183 cls.TSHARK_DISABLE_NAME_RESOLUTION, 184 cls.TSHARK_READ_FILE, pcap_path, 185 cls.TSHARK_SRC_FILTER % client.wifi_mac]) 186 if len(dut_frames): 187 raise error.TestFail('Saw unexpected frames from DUT.') 188 finally: 189 client.release_wifi_if() 190 capturer.stop_capture() 191 192 193 @classmethod 194 def assert_scanning_fails(cls, client, scan_freq): 195 """Initiates a single-channel scan, and verifies that it fails. 196 197 @param client The WiFiClient object for the DUT. 198 @param scan_freq The frequency (in MHz) on which to scan. 199 """ 200 client.claim_wifi_if() # Stop shill/supplicant scans. 201 try: 202 # We use IwRunner directly here, because WiFiClient.scan() 203 # wants a scan to succeed, while we want the scan to fail. 204 if iw_runner.IwRunner(client.host).timed_scan( 205 client.wifi_if, [scan_freq], [cls.MISSING_SSID]): 206 # We should have got None, to represent failure. 207 raise error.TestFail( 208 'Scan succeeded (and was expected to fail).') 209 finally: 210 client.release_wifi_if() 211 212 213 def fake_up_region(self, region): 214 """Modifies VPD cache to force a particular region, and reboots system 215 into to faked state. 216 217 @param region: The region we want to force the host into. 218 219 """ 220 self.host.run(self.VPD_CLEAN_COMMAND) 221 temp_vpd = tempfile.NamedTemporaryFile() 222 temp_vpd.write('"region"="%s"' % region) 223 temp_vpd.flush() 224 self.host.send_file(temp_vpd.name, self.VPD_CACHE_FILE) 225 self.host.reboot(timeout=self.REBOOT_TIMEOUT_SECS, wait=True) 226 227 228 def warmup(self, host, raw_cmdline_args, additional_params): 229 """Stashes away parameters for use by run_once(). 230 231 @param host Host object representing the client DUT. 232 @param raw_cmdline_args Raw input from autotest. 233 @param additional_params One item from CONFIGS in control file. 234 235 """ 236 self.host = host 237 self.cmdline_args = utils.args_to_dict(raw_cmdline_args) 238 self.channel_infos = additional_params['channel_infos'] 239 self.expected_country_code = additional_params['country_code'] 240 self.region_name = additional_params['region_name'] 241 242 243 def test_channel(self, wifi_context, channel_config): 244 """Verifies that a DUT's behavior on a channel is per expectations. 245 246 - Verifies that scanning behavior is per expectations. 247 - Verifies that connect behavior is per expectations. 248 - Verifies that connect behavior is the same for hidden networks, 249 as it is for visible networks. 250 251 @param wifi_context: A WiFiTestContextManager. 252 @param channel_config: A dict with 'chnum' and 'expect' keys. 253 254 """ 255 router_freq = hostap_config.HostapConfig.get_frequency_for_channel( 256 channel_config['chnum']) 257 258 # Test scanning behavior, as appropriate. To ensure that, 259 # e.g., AP beacons don't affect the DUT's behavior, this is 260 # done with no AP running. 261 if channel_config['expect'] == 'passive-scan': 262 self.assert_scanning_is_passive( 263 wifi_context.client, wifi_context.capture_host, 264 router_freq) 265 elif channel_config['expect'] == 'no-connect': 266 self.assert_scanning_fails(wifi_context.client, router_freq) 267 268 for hide_ssid in (False, True): # Simple case first. 269 self.test_connect( 270 wifi_context, 271 router_freq, 272 expect_connect=channel_config['expect'] in ( 273 'connect', 'passive-scan'), 274 hide_ssid=hide_ssid) 275 276 277 def run_once(self): 278 """Configures a DUT to behave as if it was manufactured for a 279 particular region. Then verifies that the DUT connects, or 280 fails to connect, per expectations. 281 282 """ 283 num_failures = 0 284 try: 285 self.fake_up_region(self.region_name) 286 self.assert_equal( 287 'country code', 288 iw_runner.IwRunner(self.host).get_regulatory_domain(), 289 self.expected_country_code) 290 num_mismatches = self.count_mismatched_phy_configs( 291 self.host, self.channel_infos) 292 if num_mismatches: 293 raise error.TestFail( 294 '%d phy configs were not as expected (see below)' % 295 num_mismatches) 296 wifi_context = wifi_test_context_manager.WiFiTestContextManager( 297 self.__class__.__name__, 298 self.host, 299 self.cmdline_args, 300 self.debugdir) 301 with wifi_context: 302 wifi_context.router.reboot(timeout=self.REBOOT_TIMEOUT_SECS) 303 for channel_config in self.channel_infos: 304 try: 305 self.test_channel(wifi_context, channel_config) 306 except error.TestFail as e: 307 # Log the error, but keep going. This way, we 308 # get a full report of channels where behavior 309 # differs from expectations. 310 logging.error('Verification failed for |%s|: %s', 311 self.region_name, channel_config) 312 logging.error(e) 313 num_failures += 1 314 finally: 315 if num_failures: 316 raise error.TestFail( 317 'Verification failed for %d channel configs (see below)' % 318 num_failures) 319 self.host.run(self.VPD_CLEAN_COMMAND) 320 self.host.reboot(timeout=self.REBOOT_TIMEOUT_SECS, wait=True) 321