• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#!/usr/bin/env python3
2#
3#   Copyright 2016 - 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
17import collections
18import ipaddress
19import logging
20import os
21import time
22
23from acts import logger
24from acts.controllers.ap_lib import ap_get_interface
25from acts.controllers.ap_lib import bridge_interface
26from acts.controllers.ap_lib import dhcp_config
27from acts.controllers.ap_lib import dhcp_server
28from acts.controllers.ap_lib import hostapd
29from acts.controllers.ap_lib import hostapd_config
30from acts.controllers.ap_lib import hostapd_constants
31from acts.controllers.utils_lib.commands import ip
32from acts.controllers.utils_lib.commands import route
33from acts.controllers.utils_lib.commands import shell
34from acts.controllers.utils_lib.ssh import connection
35from acts.controllers.utils_lib.ssh import settings
36from acts.libs.proc import job
37
38ACTS_CONTROLLER_CONFIG_NAME = 'AccessPoint'
39ACTS_CONTROLLER_REFERENCE_NAME = 'access_points'
40_BRCTL = 'brctl'
41
42LIFETIME = 180
43PROC_NET_SNMP6 = '/proc/net/snmp6'
44SCAPY_INSTALL_COMMAND = 'sudo python setup.py install'
45RA_MULTICAST_ADDR = '33:33:00:00:00:01'
46RA_SCRIPT = 'sendra.py'
47
48
49def create(configs):
50    """Creates ap controllers from a json config.
51
52    Creates an ap controller from either a list, or a single
53    element. The element can either be just the hostname or a dictionary
54    containing the hostname and username of the ap to connect to over ssh.
55
56    Args:
57        The json configs that represent this controller.
58
59    Returns:
60        A new AccessPoint.
61    """
62    return [AccessPoint(c) for c in configs]
63
64
65def destroy(aps):
66    """Destroys a list of access points.
67
68    Args:
69        aps: The list of access points to destroy.
70    """
71    for ap in aps:
72        ap.close()
73
74
75def get_info(aps):
76    """Get information on a list of access points.
77
78    Args:
79        aps: A list of AccessPoints.
80
81    Returns:
82        A list of all aps hostname.
83    """
84    return [ap.ssh_settings.hostname for ap in aps]
85
86
87class Error(Exception):
88    """Error raised when there is a problem with the access point."""
89
90
91_ApInstance = collections.namedtuple('_ApInstance', ['hostapd', 'subnet'])
92
93# These ranges were split this way since each physical radio can have up
94# to 8 SSIDs so for the 2GHz radio the DHCP range will be
95# 192.168.1 - 8 and the 5Ghz radio will be 192.168.9 - 16
96_AP_2GHZ_SUBNET_STR_DEFAULT = '192.168.1.0/24'
97_AP_5GHZ_SUBNET_STR_DEFAULT = '192.168.9.0/24'
98
99# The last digit of the ip for the bridge interface
100BRIDGE_IP_LAST = '100'
101
102
103class AccessPoint(object):
104    """An access point controller.
105
106    Attributes:
107        ssh: The ssh connection to this ap.
108        ssh_settings: The ssh settings being used by the ssh connection.
109        dhcp_settings: The dhcp server settings being used.
110    """
111
112    def __init__(self, configs):
113        """
114        Args:
115            configs: configs for the access point from config file.
116        """
117        self.ssh_settings = settings.from_config(configs['ssh_config'])
118        self.log = logger.create_logger(lambda msg: '[Access Point|%s] %s' % (
119            self.ssh_settings.hostname, msg))
120
121        if 'ap_subnet' in configs:
122            self._AP_2G_SUBNET_STR = configs['ap_subnet']['2g']
123            self._AP_5G_SUBNET_STR = configs['ap_subnet']['5g']
124        else:
125            self._AP_2G_SUBNET_STR = _AP_2GHZ_SUBNET_STR_DEFAULT
126            self._AP_5G_SUBNET_STR = _AP_5GHZ_SUBNET_STR_DEFAULT
127
128        self._AP_2G_SUBNET = dhcp_config.Subnet(
129            ipaddress.ip_network(self._AP_2G_SUBNET_STR))
130        self._AP_5G_SUBNET = dhcp_config.Subnet(
131            ipaddress.ip_network(self._AP_5G_SUBNET_STR))
132
133        self.ssh = connection.SshConnection(self.ssh_settings)
134
135        # Singleton utilities for running various commands.
136        self._ip_cmd = ip.LinuxIpCommand(self.ssh)
137        self._route_cmd = route.LinuxRouteCommand(self.ssh)
138
139        # A map from network interface name to _ApInstance objects representing
140        # the hostapd instance running against the interface.
141        self._aps = dict()
142        self.bridge = bridge_interface.BridgeInterface(self)
143        self.interfaces = ap_get_interface.ApInterfaces(self)
144
145        # Get needed interface names and initialize the unneccessary ones.
146        self.wan = self.interfaces.get_wan_interface()
147        self.wlan = self.interfaces.get_wlan_interface()
148        self.wlan_2g = self.wlan[0]
149        self.wlan_5g = self.wlan[1]
150        self.lan = self.interfaces.get_lan_interface()
151        self.__initial_ap()
152        self.scapy_install_path = None
153
154    def __initial_ap(self):
155        """Initial AP interfaces.
156
157        Bring down hostapd if instance is running, bring down all bridge
158        interfaces.
159        """
160        # This is necessary for Gale/Whirlwind flashed with dev channel image
161        # Unused interfaces such as existing hostapd daemon, guest, mesh
162        # interfaces need to be brought down as part of the AP initialization
163        # process, otherwise test would fail.
164        try:
165            self.ssh.run('stop wpasupplicant')
166            self.ssh.run('stop hostapd')
167        except job.Error:
168            self.log.debug('No hostapd running')
169        # Bring down all wireless interfaces
170        for iface in self.wlan:
171            WLAN_DOWN = 'ifconfig {} down'.format(iface)
172            self.ssh.run(WLAN_DOWN)
173        # Bring down all bridge interfaces
174        bridge_interfaces = self.interfaces.get_bridge_interface()
175        if bridge_interfaces:
176            for iface in bridge_interfaces:
177                BRIDGE_DOWN = 'ifconfig {} down'.format(iface)
178                BRIDGE_DEL = 'brctl delbr {}'.format(iface)
179                self.ssh.run(BRIDGE_DOWN)
180                self.ssh.run(BRIDGE_DEL)
181
182    def start_ap(self, hostapd_config, additional_parameters=None):
183        """Starts as an ap using a set of configurations.
184
185        This will start an ap on this host. To start an ap the controller
186        selects a network interface to use based on the configs given. It then
187        will start up hostapd on that interface. Next a subnet is created for
188        the network interface and dhcp server is refreshed to give out ips
189        for that subnet for any device that connects through that interface.
190
191        Args:
192            hostapd_config: hostapd_config.HostapdConfig, The configurations
193                            to use when starting up the ap.
194            additional_parameters: A dictionary of parameters that can sent
195                                   directly into the hostapd config file.  This
196                                   can be used for debugging and or adding one
197                                   off parameters into the config.
198
199        Returns:
200            An identifier for the ap being run. This identifier can be used
201            later by this controller to control the ap.
202
203        Raises:
204            Error: When the ap can't be brought up.
205        """
206
207        if hostapd_config.frequency < 5000:
208            interface = self.wlan_2g
209            subnet = self._AP_2G_SUBNET
210        else:
211            interface = self.wlan_5g
212            subnet = self._AP_5G_SUBNET
213
214        # In order to handle dhcp servers on any interface, the initiation of
215        # the dhcp server must be done after the wlan interfaces are figured
216        # out as opposed to being in __init__
217        self._dhcp = dhcp_server.DhcpServer(self.ssh, interface=interface)
218
219        # For multi bssid configurations the mac address
220        # of the wireless interface needs to have enough space to mask out
221        # up to 8 different mac addresses.  The easiest way to do this
222        # is to set the last byte to 0.  While technically this could
223        # cause a duplicate mac address it is unlikely and will allow for
224        # one radio to have up to 8 APs on the interface.
225        interface_mac_orig = None
226        cmd = "ifconfig %s|grep ether|awk -F' ' '{print $2}'" % interface
227        interface_mac_orig = self.ssh.run(cmd)
228        hostapd_config.bssid = interface_mac_orig.stdout[:-1] + '0'
229
230        if interface in self._aps:
231            raise ValueError('No WiFi interface available for AP on '
232                             'channel %d' % hostapd_config.channel)
233
234        apd = hostapd.Hostapd(self.ssh, interface)
235        new_instance = _ApInstance(hostapd=apd, subnet=subnet)
236        self._aps[interface] = new_instance
237
238        # Turn off the DHCP server, we're going to change its settings.
239        self._dhcp.stop()
240        # Clear all routes to prevent old routes from interfering.
241        self._route_cmd.clear_routes(net_interface=interface)
242
243        if hostapd_config.bss_lookup:
244            # The dhcp_bss dictionary is created to hold the key/value
245            # pair of the interface name and the ip scope that will be
246            # used for the particular interface.  The a, b, c, d
247            # variables below are the octets for the ip address.  The
248            # third octet is then incremented for each interface that
249            # is requested.  This part is designed to bring up the
250            # hostapd interfaces and not the DHCP servers for each
251            # interface.
252            dhcp_bss = {}
253            counter = 1
254            for bss in hostapd_config.bss_lookup:
255                if interface_mac_orig:
256                    hostapd_config.bss_lookup[
257                        bss].bssid = interface_mac_orig.stdout[:-1] + str(
258                            counter)
259                self._route_cmd.clear_routes(net_interface=str(bss))
260                if interface is self.wlan_2g:
261                    starting_ip_range = self._AP_2G_SUBNET_STR
262                else:
263                    starting_ip_range = self._AP_5G_SUBNET_STR
264                a, b, c, d = starting_ip_range.split('.')
265                dhcp_bss[bss] = dhcp_config.Subnet(
266                    ipaddress.ip_network('%s.%s.%s.%s' %
267                                         (a, b, str(int(c) + counter), d)))
268                counter = counter + 1
269
270        apd.start(hostapd_config, additional_parameters=additional_parameters)
271
272        # The DHCP serer requires interfaces to have ips and routes before
273        # the server will come up.
274        interface_ip = ipaddress.ip_interface(
275            '%s/%s' % (subnet.router, subnet.network.netmask))
276        self._ip_cmd.set_ipv4_address(interface, interface_ip)
277        if hostapd_config.bss_lookup:
278            # This loop goes through each interface that was setup for
279            # hostapd and assigns the DHCP scopes that were defined but
280            # not used during the hostapd loop above.  The k and v
281            # variables represent the interface name, k, and dhcp info, v.
282            for k, v in dhcp_bss.items():
283                bss_interface_ip = ipaddress.ip_interface(
284                    '%s/%s' % (dhcp_bss[k].router,
285                               dhcp_bss[k].network.netmask))
286                self._ip_cmd.set_ipv4_address(str(k), bss_interface_ip)
287
288        # Restart the DHCP server with our updated list of subnets.
289        configured_subnets = [x.subnet for x in self._aps.values()]
290        if hostapd_config.bss_lookup:
291            for k, v in dhcp_bss.items():
292                configured_subnets.append(v)
293
294        self._dhcp.start(config=dhcp_config.DhcpConfig(configured_subnets))
295
296        # The following three commands are needed to enable bridging between
297        # the WAN and LAN/WLAN ports.  This means anyone connecting to the
298        # WLAN/LAN ports will be able to access the internet if the WAN port
299        # is connected to the internet.
300        self.ssh.run('iptables -t nat -F')
301        self.ssh.run(
302            'iptables -t nat -A POSTROUTING -o %s -j MASQUERADE' % self.wan)
303        self.ssh.run('echo 1 > /proc/sys/net/ipv4/ip_forward')
304
305        return interface
306
307    def get_bssid_from_ssid(self, ssid, band):
308        """Gets the BSSID from a provided SSID
309
310        Args:
311            ssid: An SSID string.
312            band: 2G or 5G Wifi band.
313        Returns: The BSSID if on the AP or None if SSID could not be found.
314        """
315        if band == hostapd_constants.BAND_2G:
316            interfaces = [self.wlan_2g, ssid]
317        else:
318            interfaces = [self.wlan_5g, ssid]
319
320        # Get the interface name associated with the given ssid.
321        for interface in interfaces:
322            cmd = "iw dev %s info|grep ssid|awk -F' ' '{print $2}'" % (
323                str(interface))
324            iw_output = self.ssh.run(cmd)
325            if 'command failed: No such device' in iw_output.stderr:
326                continue
327            else:
328                # If the configured ssid is equal to the given ssid, we found
329                # the right interface.
330                if iw_output.stdout == ssid:
331                    cmd = "iw dev %s info|grep addr|awk -F' ' '{print $2}'" % (
332                        str(interface))
333                    iw_output = self.ssh.run(cmd)
334                    return iw_output.stdout
335        return None
336
337    def stop_ap(self, identifier):
338        """Stops a running ap on this controller.
339
340        Args:
341            identifier: The identify of the ap that should be taken down.
342        """
343
344        if identifier not in list(self._aps.keys()):
345            raise ValueError('Invalid identifier %s given' % identifier)
346
347        instance = self._aps.get(identifier)
348
349        instance.hostapd.stop()
350        self._dhcp.stop()
351        self._ip_cmd.clear_ipv4_addresses(identifier)
352
353        # DHCP server needs to refresh in order to tear down the subnet no
354        # longer being used. In the event that all interfaces are torn down
355        # then an exception gets thrown. We need to catch this exception and
356        # check that all interfaces should actually be down.
357        configured_subnets = [x.subnet for x in self._aps.values()]
358        del self._aps[identifier]
359        if configured_subnets:
360            self._dhcp.start(dhcp_config.DhcpConfig(configured_subnets))
361
362    def stop_all_aps(self):
363        """Stops all running aps on this device."""
364
365        for ap in list(self._aps.keys()):
366            try:
367                self.stop_ap(ap)
368            except dhcp_server.NoInterfaceError as e:
369                pass
370
371    def close(self):
372        """Called to take down the entire access point.
373
374        When called will stop all aps running on this host, shutdown the dhcp
375        server, and stop the ssh connection.
376        """
377
378        if self._aps:
379            self.stop_all_aps()
380        self.ssh.close()
381
382    def generate_bridge_configs(self, channel):
383        """Generate a list of configs for a bridge between LAN and WLAN.
384
385        Args:
386            channel: the channel WLAN interface is brought up on
387            iface_lan: the LAN interface to bridge
388        Returns:
389            configs: tuple containing iface_wlan, iface_lan and bridge_ip
390        """
391
392        if channel < 15:
393            iface_wlan = self.wlan_2g
394            subnet_str = self._AP_2G_SUBNET_STR
395        else:
396            iface_wlan = self.wlan_5g
397            subnet_str = self._AP_5G_SUBNET_STR
398
399        iface_lan = self.lan
400
401        a, b, c, d = subnet_str.strip('/24').split('.')
402        bridge_ip = "%s.%s.%s.%s" % (a, b, c, BRIDGE_IP_LAST)
403
404        configs = (iface_wlan, iface_lan, bridge_ip)
405
406        return configs
407
408    def install_scapy(self, scapy_path, send_ra_path):
409        """Install scapy
410
411        Args:
412            scapy_path: path where scapy tar file is located on server
413            send_ra_path: path where sendra path is located on server
414        """
415        self.scapy_install_path = self.ssh.run('mktemp -d').stdout.rstrip()
416        self.log.info("Scapy install path: %s" % self.scapy_install_path)
417        self.ssh.send_file(scapy_path, self.scapy_install_path)
418        self.ssh.send_file(send_ra_path, self.scapy_install_path)
419
420        scapy = os.path.join(self.scapy_install_path, scapy_path.split('/')[-1])
421
422        untar_res = self.ssh.run(
423            'tar -xvf %s -C %s' % (scapy, self.scapy_install_path))
424
425        instl_res = self.ssh.run(
426            'cd %s; %s' % (self.scapy_install_path, SCAPY_INSTALL_COMMAND))
427
428    def cleanup_scapy(self):
429        """ Cleanup scapy """
430        if self.scapy_install_path:
431            cmd = 'rm -rf %s' % self.scapy_install_path
432            self.log.info("Cleaning up scapy %s" % cmd)
433            output = self.ssh.run(cmd)
434            self.scapy_install_path = None
435
436    def send_ra(self, iface, mac=RA_MULTICAST_ADDR, interval=1, count=None,
437                lifetime=LIFETIME, rtt=0):
438        """Invoke scapy and send RA to the device.
439
440        Args:
441          iface: string of the WiFi interface to use for sending packets.
442          mac: string HWAddr/MAC address to send the packets to.
443          interval: int Time to sleep between consecutive packets.
444          count: int Number of packets to be sent.
445          lifetime: int original RA's router lifetime in seconds.
446          rtt: retrans timer of the RA packet
447        """
448        scapy_command = os.path.join(self.scapy_install_path, RA_SCRIPT)
449        options = ' -m %s -i %d -c %d -l %d -in %s -rtt %s' % (
450            mac, interval, count, lifetime, iface, rtt)
451        self.log.info("Scapy cmd: %s" % scapy_command + options)
452        res = self.ssh.run(scapy_command + options)
453
454    def get_icmp6intype134(self):
455        """Read the value of Icmp6InType134 and return integer.
456
457        Returns:
458            Integer value >0 if grep is successful; 0 otherwise.
459        """
460        ra_count_str = self.ssh.run('grep Icmp6InType134 %s || true' %
461                                    PROC_NET_SNMP6).stdout
462        if ra_count_str:
463            return int(ra_count_str.split()[1])
464