site_linux_router.py revision 7b23a544ba76d38c6b8caa0714b736040186efb4
1# Copyright (c) 2010 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 logging, re, time
6from autotest_lib.client.common_lib import error
7from autotest_lib.server import site_linux_system
8
9def isLinuxRouter(router):
10    router_uname = router.run('uname').stdout
11    return re.search('Linux', router_uname)
12
13class LinuxRouter(site_linux_system.LinuxSystem):
14    """
15    Linux/mac80211-style WiFi Router support for WiFiTest class.
16
17    This class implements test methods/steps that communicate with a
18    router implemented with Linux/mac80211.  The router must
19    be pre-configured to enable ssh access and have a mac80211-based
20    wireless device.  We also assume hostapd 0.7.x and iw are present
21    and any necessary modules are pre-loaded.
22    """
23
24
25    def __init__(self, host, params, defssid):
26        site_linux_system.LinuxSystem.__init__(self, host, params, "router")
27        self._remove_interfaces()
28
29        self.cmd_hostapd = params.get("cmd_hostapd", "/usr/sbin/hostapd")
30        self.cmd_hostapd_cli = \
31            params.get("cmd_hostapd_cli", "/usr/sbin/hostapd_cli")
32        self.dhcpd_conf = "/tmp/dhcpd.conf"
33        self.dhcpd_leases = "/tmp/dhcpd.leases"
34
35        # Router host.
36        self.router = host
37
38        # hostapd configuration persists throughout the test, subsequent
39        # 'config' commands only modify it.
40        self.defssid = defssid
41        self.hostapd = {
42            'configured': False,
43            'file': "/tmp/hostapd-test.conf",
44            'log': "/tmp/hostapd-test.log",
45            'log_count': 0,
46            'driver': "nl80211",
47            'conf': {
48                'ssid': defssid,
49                'hw_mode': 'g',
50                'ctrl_interface': '/tmp/hostapd-test.control',
51                'logger_syslog': '-1',
52                'logger_syslog_level': '0'
53            }
54        }
55        self.station = {
56            'configured': False,
57            'conf': {
58                'ssid': defssid,
59            },
60        }
61        self.default_interface = None
62        self.local_servers = []
63        self.force_local_server = "force_local_server" in params
64        self.dhcp_low = 1
65        self.dhcp_high = 128
66
67        # Kill hostapd if already running.
68        self.kill_hostapd()
69
70        # Place us in the US by default
71        self.router.run("%s reg set US" % self.cmd_iw)
72
73
74    def create(self, params):
75        """ Create a wifi device of the specified type """
76        #
77        # AP mode is handled entirely by hostapd so we only
78        # have to setup others (mapping the bsd type to what
79        # iw wants)
80        #
81        # map from bsd types to iw types
82        self.apmode = params['type'] in ("ap", "hostap")
83        if not self.apmode:
84            self.station['type'] = params['type']
85        self.phytype = {
86            "sta"       : "managed",
87            "monitor"   : "monitor",
88            "adhoc"     : "adhoc",
89            "ibss"      : "ibss",
90            "ap"        : "managed",     # NB: handled by hostapd
91            "hostap"    : "managed",     # NB: handled by hostapd
92            "mesh"      : "mesh",
93            "wds"       : "wds",
94        }[params['type']]
95
96
97    def destroy(self, params):
98        """ Destroy a previously created device """
99        # For linux, this is the same as deconfig.
100        self.deconfig(params)
101
102    def has_local_server(self):
103        return bool(self.local_servers)
104
105    def cleanup(self, params):
106        """ Clean up any resources in use """
107        # For linux, this is a no-op
108        pass
109
110    def kill_hostapd(self):
111        """
112        Kills the hostapd process.  Makes sure hostapd exits before
113        continuing since it sets the interface back to station mode in its
114        cleanup path.  If we start another instance of hostapd before the
115        previous instance exits, the interface station mode will overwrite the
116        ap mode.
117        """
118        self.router.run("pkill hostapd >/dev/null 2>&1 && "
119                        "while pgrep hostapd &> /dev/null; do sleep 1; done",
120                        timeout=30,
121                        ignore_status=True)
122
123    def hostap_config(self, params):
124        """ Configure the AP per test requirements """
125
126        # keep parameter modifications local-only
127        orig_params = params
128        params = params.copy()
129
130        multi_interface = 'multi_interface' in params
131        if multi_interface:
132            # remove non-hostapd config item from params
133            params.pop('multi_interface')
134        elif self.hostapd['configured'] or self.station['configured']:
135            self.deconfig({})
136
137        local_server = params.pop('local_server', False)
138
139        # Construct the hostapd.conf file and start hostapd.
140        conf = self.hostapd['conf']
141        # default RTS and frag threshold to ``off''
142        conf['rts_threshold'] = '2347'
143        conf['fragm_threshold'] = '2346'
144
145        tx_power_params = {}
146        htcaps = set()
147
148        conf['driver'] = params.get('hostapd_driver',
149            self.hostapd['driver'])
150
151        for k, v in params.iteritems():
152            if k == 'ssid':
153                conf['ssid'] = v
154            elif k == 'ssid_suffix':
155                conf['ssid'] = self.defssid + v
156            elif k == 'channel':
157                freq = int(v)
158                self.hostapd['frequency'] = freq
159
160                # 2.4GHz
161                if freq <= 2484:
162                    # Make sure hw_mode is set
163                    if conf.get('hw_mode') == 'a':
164                        conf['hw_mode'] = 'g'
165
166                    # Freq = 5 * chan + 2407, except channel 14
167                    if freq == 2484:
168                        conf['channel'] = 14
169                    else:
170                        conf['channel'] = (freq - 2407) / 5
171                # 5GHz
172                else:
173                    # Make sure hw_mode is set
174                    conf['hw_mode'] = 'a'
175                    # Freq = 5 * chan + 4000
176                    if freq < 5000:
177                        conf['channel'] = (freq - 4000) / 5
178                    # Freq = 5 * chan + 5000
179                    else:
180                        conf['channel'] = (freq - 5000) / 5
181
182            elif k == 'country':
183                conf['country_code'] = v
184            elif k == 'dotd':
185                conf['ieee80211d'] = 1
186            elif k == '-dotd':
187                conf['ieee80211d'] = 0
188            elif k == 'mode':
189                if v == '11a':
190                    conf['hw_mode'] = 'a'
191                elif v == '11g':
192                    conf['hw_mode'] = 'g'
193                elif v == '11b':
194                    conf['hw_mode'] = 'b'
195                elif v == '11n':
196                    conf['ieee80211n'] = 1
197            elif k == 'bintval':
198                conf['beacon_int'] = v
199            elif k == 'dtimperiod':
200                conf['dtim_period'] = v
201            elif k == 'rtsthreshold':
202                conf['rts_threshold'] = v
203            elif k == 'fragthreshold':
204                conf['fragm_threshold'] = v
205            elif k == 'shortpreamble':
206                conf['preamble'] = 1
207            elif k == 'authmode':
208                if v == "open":
209                    conf['auth_algs'] = 1
210                elif v == "shared":
211                    conf['auth_algs'] = 2
212            elif k == 'hidessid':
213                conf['ignore_broadcast_ssid'] = 1
214            elif k == 'wme':
215                conf['wmm_enabled'] = 1
216            elif k == '-wme':
217                conf['wmm_enabled'] = 0
218            elif k == 'deftxkey':
219                conf['wep_default_key'] = v
220            elif k == 'ht20':
221                htcaps.add('')  # NB: ensure 802.11n setup below
222                conf['wmm_enabled'] = 1
223            elif k == 'ht40':
224                htcaps.add('[HT40-]')
225                htcaps.add('[HT40+]')
226                conf['wmm_enabled'] = 1
227            elif k in ('ht40+', 'ht40-'):
228                htcaps.add('[%s]' % k.upper())
229                conf['wmm_enabled'] = 1
230            elif k == 'shortgi':
231                htcaps.add('[SHORT-GI-20]')
232                htcaps.add('[SHORT-GI-40]')
233            elif k == 'pureg':
234                pass        # TODO(sleffler) need hostapd support
235            elif k == 'puren':
236                pass        # TODO(sleffler) need hostapd support
237            elif k == 'protmode':
238                pass        # TODO(sleffler) need hostapd support
239            elif k == 'ht':
240                htcaps.add('')  # NB: ensure 802.11n setup below
241            elif k == 'htprotmode':
242                pass        # TODO(sleffler) need hostapd support
243            elif k == 'rifs':
244                pass        # TODO(sleffler) need hostapd support
245            elif k == 'wepmode':
246                pass        # NB: meaningless for hostapd; ignore
247            elif k == '-ampdu':
248                pass        # TODO(sleffler) need hostapd support
249            elif k == 'txpower':
250                tx_power_params['power'] = v
251            else:
252                conf[k] = v
253
254        # Aggregate ht_capab.
255        if htcaps:
256            conf['ieee80211n'] = 1
257            conf['ht_capab'] = ''.join(htcaps)
258
259        # Figure out the correct interface.
260        conf['interface'] = self._get_wlanif(self.hostapd['frequency'],
261                                             self.phytype,
262                                             mode=conf.get('hw_mode', 'b'))
263
264        # Generate hostapd.conf.
265        self._pre_config_hook(conf)
266        self.router.run("cat <<EOF >%s\n%s\nEOF\n" %
267            (self.hostapd['file'], '\n'.join(
268            "%s=%s" % kv for kv in conf.iteritems())))
269
270        self._pre_start_hook(orig_params)
271
272        # Run hostapd.
273        logging.info("Starting hostapd...")
274        self.router.run("%s -dd %s &> %s &" %
275            (self.cmd_hostapd, self.hostapd['file'], self.hostapd['log']))
276
277        if not multi_interface:
278            self.default_interface = conf['interface']
279
280        # Configure transmit power
281        tx_power_params['interface'] = conf['interface']
282        self.set_txpower(tx_power_params)
283
284        if self.force_local_server or local_server is not False:
285            self.start_local_server(conf['interface'])
286
287        self._post_start_hook(orig_params)
288
289        logging.info("AP configured.")
290        self.hostapd['configured'] = True
291
292    @staticmethod
293    def ip_addr(netblock, idx):
294        """
295        Simple IPv4 calculator.  Takes host address in "IP/bits" notation
296        and returns netmask, broadcast address as well as integer offsets
297        into the address range.
298        """
299        addr_str,bits = netblock.split('/')
300        addr = map(int, addr_str.split('.'))
301        mask_bits = (-1 << (32-int(bits))) & 0xffffffff
302        mask = [(mask_bits >> s) & 0xff for s in range(24, -1, -8)]
303        if idx == 'local':
304            return addr_str
305        elif idx == 'netmask':
306            return '.'.join(map(str, mask))
307        elif idx == 'broadcast':
308            offset = [m ^ 0xff for m in mask]
309        else:
310            offset = [(idx >> s) & 0xff for s in range(24, -1, -8)]
311        return '.'.join(map(str, [(a & m) + o
312                                  for a, m, o in zip(addr, mask, offset)]))
313
314
315    def station_config(self, params):
316        # keep parameter modifications local-only
317        orig_params = params
318        params = params.copy()
319
320        if 'multi_interface' in params:
321            raise NotImplementedError("station with multi_interface")
322
323        if self.station['type'] != 'ibss':
324            raise NotImplementedError("non-ibss station")
325
326        if self.station['configured'] or self.hostapd['configured']:
327            self.deconfig({})
328
329        local_server = params.pop('local_server', False)
330        mode = None
331        conf = self.station['conf']
332        for k, v in params.iteritems():
333            if k == 'ssid_suffix':
334                conf['ssid'] = self.defssid + v
335            elif k == 'channel':
336                freq = int(v)
337                if freq > 2484:
338                    mode = 'a'
339            elif k == 'mode':
340                if v == '11a':
341                    mode = 'a'
342            else:
343                conf[k] = v
344
345        interface = self._get_wlanif(freq, self.phytype, mode)
346
347        # Run interface configuration commands
348        for k, v in conf.iteritems():
349            if k != 'ssid':
350                self.router.run("%s dev %s set %s %s" %
351                                (self.cmd_iw, interface, k, v))
352
353        # Connect the station
354        self.router.run("%s link set %s up" % (self.cmd_ip, interface))
355        self.router.run("%s dev %s ibss join %s %d" %
356                        (self.cmd_iw, interface, conf['ssid'], freq))
357
358        if self.force_local_server or local_server is not False:
359            self.start_local_server(interface)
360
361        self.station['configured'] = True
362        self.station['interface'] = interface
363
364
365    def start_local_server(self, interface):
366        logging.info("Starting up local server...")
367
368        if len(self.local_servers) >= 256:
369            raise error.TestFail('Exhausted available local servers')
370
371        netblock = '%d.%d.%d.%d/24' % \
372            (192, 168, len(self.local_servers), 254)
373
374        params = {}
375        params['netblock'] = netblock
376        params['subnet'] = self.ip_addr(netblock, 0)
377        params['netmask'] = self.ip_addr(netblock, 'netmask')
378        params['dhcp_range'] = ' '.join(
379            (self.ip_addr(netblock, self.dhcp_low),
380             self.ip_addr(netblock, self.dhcp_high)))
381        params['interface'] = interface
382
383        params['ip_params'] = ("%s broadcast %s dev %s" %
384                               (netblock,
385                                self.ip_addr(netblock, 'broadcast'),
386                                interface))
387        self.local_servers.append(params)
388
389        self.router.run("%s addr flush %s" %
390                        (self.cmd_ip, interface))
391        self.router.run("%s addr add %s" %
392                        (self.cmd_ip, params['ip_params']))
393        self.router.run("%s link set %s up" %
394                        (self.cmd_ip, interface))
395        self.start_dhcp_server()
396
397    def start_dhcp_server(self):
398        dhcp_conf = '\n'.join(map(
399            lambda server_conf: \
400                "subnet %(subnet)s netmask %(netmask)s {\n" \
401                "  range %(dhcp_range)s;\n" \
402                "}" % server_conf,
403            self.local_servers))
404        self.router.run("cat <<EOF >%s\n%s\nEOF\n" %
405            (self.dhcpd_conf,
406             '\n'.join(('ddns-update-style none;', dhcp_conf))))
407        self.router.run("touch %s" % self.dhcpd_leases)
408
409        self.router.run("pkill dhcpd >/dev/null 2>&1", ignore_status=True)
410        self.router.run("%s -q -cf %s -lf %s" %
411                        (self.cmd_dhcpd, self.dhcpd_conf, self.dhcpd_leases))
412
413
414    def config(self, params):
415        if self.apmode:
416            self.hostap_config(params)
417        else:
418            self.station_config(params)
419
420
421    def get_wifi_ip(self, ap_num):
422        if self.local_servers:
423            return self.ip_addr(self.local_servers[ap_num]['netblock'],
424                                'local')
425        else:
426            raise error.TestFail("No IP address assigned")
427
428
429    def deconfig(self, params):
430        """ De-configure the AP (will also bring wlan down) """
431
432        if not self.hostapd['configured'] and not self.station['configured']:
433            return
434
435        # Taking down hostapd takes wlan0 and mon.wlan0 down.
436        if self.hostapd['configured']:
437            if 'silent' in params:
438                # Deconfigure without notifying DUT.  Remove the monitor
439                # interface hostapd uses to send beacon and DEAUTH packets
440                self._remove_interfaces()
441
442            self.kill_hostapd()
443#           self.router.run("rm -f %s" % self.hostapd['file'])
444            self.router.get_file(self.hostapd['log'],
445                                 'debug/hostapd_router_%d.log' %
446                                 self.hostapd['log_count'])
447            self.hostapd['log_count'] += 1
448        if self.station['configured']:
449            if self.station['type'] == 'ibss':
450                self.router.run("%s dev %s ibss leave" %
451                                (self.cmd_iw, self.station['interface']))
452            else:
453                self.router.run("%s dev %s disconnect" %
454                                (self.cmd_iw, self.station['interface']))
455            self.router.run("%s link set %s down" % (self.cmd_ip,
456                                                     self.station['interface']))
457
458        if self.local_servers:
459            self.router.run("pkill dhcpd >/dev/null 2>&1",
460                            ignore_status=True)
461            for server in self.local_servers:
462                self.router.run("%s addr del %s" %
463                                (self.cmd_ip, server['ip_params']))
464            self.local_servers = []
465
466        self.hostapd['configured'] = False
467        self.station['configured'] = False
468
469
470    def get_ssid(self):
471        return self.hostapd['conf']['ssid']
472
473
474    def set_txpower(self, params):
475        self.router.run("%s dev %s set txpower %s" %
476                        (self.cmd_iw, params.get('interface',
477                                                 self.default_interface),
478                         params.get('power', 'auto')))
479
480
481    def deauth(self, params):
482        self.router.run('%s -p%s deauthenticate %s' %
483                        (self.cmd_hostapd_cli,
484                         self.hostapd['conf']['ctrl_interface'],
485                         params['client']))
486
487
488    def _pre_config_hook(self, config):
489        """
490        Hook for subclasses. Run after gathering configuration parameters,
491        but before writing parameters to config file.
492        """
493        pass
494
495
496    def _pre_start_hook(self, params):
497        """
498        Hook for subclasses. Run after generating hostapd config file, but
499        before starting hostapd.
500        """
501        pass
502
503
504    def _post_start_hook(self, params):
505        """Hook for subclasses. Run after starting hostapd."""
506        pass
507