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 os 6import subprocess 7 8from autotest_lib.client.bin import utils 9from autotest_lib.client.common_lib.cros import site_eap_certs 10 11class HostapdServer(object): 12 """Hostapd server instance wrapped in a context manager. 13 14 Simple interface to starting and controlling a hsotapd instance. 15 This can be combined with a virtual-ethernet setup to test 802.1x 16 on a wired interface. 17 18 Example usage: 19 with hostapd_server.HostapdServer(interface='veth_master') as hostapd: 20 hostapd.send_eap_packets() 21 22 """ 23 CONFIG_TEMPLATE = """ 24interface=%(interface)s 25driver=%(driver)s 26logger_syslog=-1 27logger_syslog_level=2 28logger_stdout=-1 29logger_stdout_level=2 30dump_file=%(config_directory)s/hostapd.dump 31ctrl_interface=%(config_directory)s/%(control_directory)s 32ieee8021x=1 33eapol_key_index_workaround=0 34eap_server=1 35eap_user_file=%(config_directory)s/%(user_file)s 36ca_cert=%(config_directory)s/%(ca_cert)s 37server_cert=%(config_directory)s/%(server_cert)s 38private_key=%(config_directory)s/%(server_key)s 39use_pae_group_addr=1 40eap_reauth_period=10 41""" 42 CA_CERTIFICATE_FILE = 'ca.crt' 43 CONFIG_FILE = 'hostapd.conf' 44 CONTROL_DIRECTORY = 'hostapd.ctl' 45 EAP_PASSWORD = 'password' 46 EAP_PHASE2 = 'MSCHAPV2' 47 EAP_TYPE = 'PEAP' 48 EAP_USERNAME = 'test' 49 HOSTAPD_EXECUTABLE = 'hostapd' 50 HOSTAPD_CLIENT_EXECUTABLE = 'hostapd_cli' 51 SERVER_CERTIFICATE_FILE = 'server.crt' 52 SERVER_PRIVATE_KEY_FILE = 'server.key' 53 USER_AUTHENTICATION_TEMPLATE = """* %(type)s 54"%(username)s"\t%(phase2)s\t"%(password)s"\t[2] 55""" 56 USER_FILE = 'hostapd.eap_user' 57 # This is the default group MAC address to which EAP challenges 58 # are sent, absent any prior knowledge of a specific client on 59 # the link. 60 PAE_NEAREST_ADDRESS = '01:80:c2:00:00:03' 61 62 def __init__(self, 63 interface=None, 64 driver='wired', 65 config_directory='/tmp/hostapd-test'): 66 super(HostapdServer, self).__init__() 67 self._interface = interface 68 self._config_directory = config_directory 69 self._control_directory = '%s/%s' % (self._config_directory, 70 self.CONTROL_DIRECTORY) 71 self._driver = driver 72 self._process = None 73 74 75 def __enter__(self): 76 self.start() 77 return self 78 79 80 def __exit__(self, exception, value, traceback): 81 self.stop() 82 83 84 def write_config(self): 85 """Write out a hostapd configuration file-set based on the caller 86 supplied parameters. 87 88 @return the file name of the top-level configuration file written. 89 90 """ 91 if not os.path.exists(self._config_directory): 92 os.mkdir(self._config_directory) 93 config_params = { 94 'ca_cert': self.CA_CERTIFICATE_FILE, 95 'config_directory' : self._config_directory, 96 'control_directory': self.CONTROL_DIRECTORY, 97 'driver': self._driver, 98 'interface': self._interface, 99 'server_cert': self.SERVER_CERTIFICATE_FILE, 100 'server_key': self.SERVER_PRIVATE_KEY_FILE, 101 'user_file': self.USER_FILE 102 } 103 authentication_params = { 104 'password': self.EAP_PASSWORD, 105 'phase2': self.EAP_PHASE2, 106 'username': self.EAP_USERNAME, 107 'type': self.EAP_TYPE 108 } 109 for filename, contents in ( 110 ( self.CA_CERTIFICATE_FILE, site_eap_certs.ca_cert_1 ), 111 ( self.CONFIG_FILE, self.CONFIG_TEMPLATE % config_params), 112 ( self.SERVER_CERTIFICATE_FILE, site_eap_certs.server_cert_1 ), 113 ( self.SERVER_PRIVATE_KEY_FILE, 114 site_eap_certs.server_private_key_1 ), 115 ( self.USER_FILE, 116 self.USER_AUTHENTICATION_TEMPLATE % authentication_params )): 117 config_file = '%s/%s' % (self._config_directory, filename) 118 with open(config_file, 'w') as f: 119 f.write(contents) 120 return '%s/%s' % (self._config_directory, self.CONFIG_FILE) 121 122 123 def start(self): 124 """Start the hostap server.""" 125 config_file = self.write_config() 126 self._process = subprocess.Popen( 127 [self.HOSTAPD_EXECUTABLE, '-dd', config_file]) 128 129 130 def stop(self): 131 """Stop the hostapd server.""" 132 if self._process: 133 self._process.terminate() 134 self._process.wait() 135 self._process = None 136 137 138 def running(self): 139 """Tests whether the hostapd process is still running. 140 141 @return True if the hostapd process is still running, False otherwise. 142 143 """ 144 if not self._process: 145 return False 146 147 if self._process.poll() != None: 148 # We have essentially reaped the proces, and it is no more. 149 self._process = None 150 return False 151 152 return True 153 154 155 def send_eap_packets(self): 156 """Start sending EAP packets to the nearest neighbor.""" 157 self.send_command('new_sta %s' % self.PAE_NEAREST_ADDRESS) 158 159 160 def get_client_mib(self, client_mac_address): 161 """Get a dict representing the MIB properties for |client_mac_address|. 162 163 @param client_mac_address string MAC address of the client. 164 @return dict containing mib properties. 165 166 """ 167 # Expected output of "hostapd cli <client_mac_address>": 168 # 169 # Selected interface 'veth_master' 170 # b6:f1:39:1d:ad:10 171 # dot1xPaePortNumber=0 172 # dot1xPaePortProtocolVersion=2 173 # [...] 174 result = self.send_command('sta %s' % client_mac_address) 175 client_mib = {} 176 found_client = False 177 for line in result.splitlines(): 178 if found_client: 179 parts = line.split('=', 1) 180 if len(parts) == 2: 181 client_mib[parts[0]] = parts[1] 182 elif line == client_mac_address: 183 found_client = True 184 return client_mib 185 186 187 def send_command(self, command): 188 """Send a command to the hostapd instance. 189 190 @param command string containing the command to run on hostapd. 191 @return string output of the command. 192 193 """ 194 return utils.system_output('%s -p %s %s' % 195 (self.HOSTAPD_CLIENT_EXECUTABLE, 196 self._control_directory, command)) 197 198 199 def client_has_authenticated(self, client_mac_address): 200 """Return whether |client_mac_address| has successfully authenticated. 201 202 @param client_mac_address string MAC address of the client. 203 @return True if client is authenticated. 204 205 """ 206 mib = self.get_client_mib(client_mac_address) 207 return mib.get('dot1xAuthAuthSuccessesWhileAuthenticating', '') == '1' 208 209 210 def client_has_logged_off(self, client_mac_address): 211 """Return whether |client_mac_address| has logged-off. 212 213 @param client_mac_address string MAC address of the client. 214 @return True if client has logged off. 215 216 """ 217 mib = self.get_client_mib(client_mac_address) 218 return mib.get('dot1xAuthAuthEapLogoffWhileAuthenticated', '') == '1' 219