/bin/wifi:  WDS/4-address mode support for APs.

Adds a --wds flag that turns on 4-address mode for the AP interface,
and enables hostapd support.

Change-Id: I9406a5c5cf6f8d09cada72876d602e88605cf8da
diff --git a/wifi/configs.py b/wifi/configs.py
index e83ee68..23eac7f 100644
--- a/wifi/configs.py
+++ b/wifi/configs.py
@@ -94,6 +94,7 @@
 {require_vht}
 {hidden}
 {ap_isolate}
+{wds}
 {vendor_elements}
 
 ht_capab={ht20}{ht40}{guard_interval}{ht_rxstbc}
@@ -288,6 +289,7 @@
   hidden = 'ignore_broadcast_ssid=1' if opt.hidden_mode else ''
   bridge = 'bridge=%s' % opt.bridge if opt.bridge else ''
   ap_isolate = 'ap_isolate=1' if opt.client_isolation else ''
+  wds = 'wds_sta=1' if opt.wds else ''
   hostapd_conf_parts = [_HOSTCONF_TPL.format(
       interface=interface, band=band, channel=channel, width=width,
       protocols=protocols, hostapd_band=hostapd_band,
@@ -296,7 +298,7 @@
       ht_rxstbc=ht_rxstbc, vht_settings=vht_settings,
       guard_interval=guard_interval, enable_wmm=enable_wmm, hidden=hidden,
       ap_isolate=ap_isolate, auth_algs=auth_algs, bridge=bridge,
-      ssid=utils.sanitize_ssid(opt.ssid),
+      ssid=utils.sanitize_ssid(opt.ssid), wds=wds,
       vendor_elements=get_vendor_elements(opt))]
 
   if opt.encryption != 'NONE':
diff --git a/wifi/configs_test.py b/wifi/configs_test.py
index 68e7b2a..e088298 100755
--- a/wifi/configs_test.py
+++ b/wifi/configs_test.py
@@ -352,6 +352,32 @@
 
 
 
+
+ht_capab=[HT20][RX-STBC1]
+
+"""
+
+_HOSTAPD_CONFIG_WDS = """ctrl_interface=/var/run/hostapd
+interface=wlan0
+
+ssid=TEST_SSID
+utf8_ssid=1
+auth_algs=1
+hw_mode=g
+channel=1
+country_code=US
+ieee80211d=1
+ieee80211h=1
+ieee80211n=1
+
+
+
+
+
+
+wds_sta=1
+
+
 ht_capab=[HT20][RX-STBC1]
 
 """
@@ -376,6 +402,7 @@
 
 
 
+
 ht_capab=[HT20][RX-STBC1]
 
 """
@@ -398,6 +425,7 @@
 
 ignore_broadcast_ssid=1
 
+
 vendor_elements=dd04f4f5e801dd0df4f5e803544553545f53534944
 
 ht_capab=[HT20][RX-STBC1]
@@ -435,6 +463,7 @@
     self.client_isolation = False
     self.supports_provisioning = False
     self.no_band_restriction = False
+    self.wds = False
 
 
 def wpa_passphrase(ssid, passphrase):
@@ -505,6 +534,17 @@
                   config)
   opt.bridge = default_bridge
 
+  # Test WDS.
+  default_wds, opt.wds = opt.wds, True
+  config = configs.generate_hostapd_config(
+      _PHY_INFO, 'wlan0', '2.4', '1', '20', set(('a', 'b', 'g', 'n', 'ac')),
+      'asdfqwer', opt)
+  wvtest.WVPASSEQ('\n'.join((_HOSTAPD_CONFIG_WDS,
+                             _HOSTAPD_CONFIG_WPA,
+                             '# Experiments: ()\n')),
+                  config)
+  opt.wds = default_wds
+
   # Test provisioning IEs.
   default_hidden_mode, opt.hidden_mode = opt.hidden_mode, True
   default_supports_provisioning, opt.supports_provisioning = (
diff --git a/wifi/iw.py b/wifi/iw.py
index f16c50b..0110267 100644
--- a/wifi/iw.py
+++ b/wifi/iw.py
@@ -421,3 +421,11 @@
 def scan(interface, scan_args):
   """Return 'iw scan' output for printing."""
   return _scan(interface, scan_args)
+
+
+def set_4address_mode(interface, on):
+  try:
+    setting = 'on' if on else 'off'
+    subprocess.check_output(['iw', 'dev', interface, 'set', '4addr', setting])
+  except subprocess.CalledProcessError as e:
+    raise utils.BinWifiException('Failed to set 4addr mode %s: %s', setting, e)
diff --git a/wifi/utils.py b/wifi/utils.py
index 3b2db84..33f3e53 100644
--- a/wifi/utils.py
+++ b/wifi/utils.py
@@ -196,19 +196,20 @@
     return ''
 
 
-def validate_set_wifi_options(band, width, autotype, protocols, encryption):
+def validate_set_wifi_options(opt):
   """Validates options to set_wifi.
 
   Args:
-    band: The specified band, as a string; '2.4' or '5'.
-    width: The specified channel width.
-    autotype: The specified autotype.
-    protocols: The specified 802.11 levels, as a collection of strings.
-    encryption: The specified encryption type.
+    opt: The options to validate.
 
   Raises:
     BinWifiException: if anything is not valid.
   """
+  band = opt.band
+  width = opt.width
+  autotype = opt.autotype
+  protocols = set(opt.protocols.split('/'))
+
   if band not in ('2.4', '5'):
     raise BinWifiException('You must specify band with -b2.4 or -b5')
 
@@ -240,11 +241,14 @@
   elif width == '80' and 'ac' not in protocols:
     raise BinWifiException('-p ac is needed for 40 MHz channels')
 
-  if encryption == 'WEP' or '_PSK_' in encryption:
+  if opt.encryption == 'WEP' or '_PSK_' in opt.encryption:
     if 'WIFI_PSK' not in os.environ:
       raise BinWifiException(
           'Encryption enabled; use WIFI_PSK=whatever wifi set ...')
 
+  if opt.wds and not opt.bridge:
+    raise BinWifiException('WDS mode enabled; must specify a bridge.')
+
 
 def sanitize_ssid(ssid):
   """Remove control and non-UTF8 characters from an SSID.
diff --git a/wifi/utils_test.py b/wifi/utils_test.py
index 8febea3..24e8716 100755
--- a/wifi/utils_test.py
+++ b/wifi/utils_test.py
@@ -2,13 +2,13 @@
 
 """Tests for utils.py."""
 
-import collections
 import multiprocessing
 import os
 import shutil
 import sys
 import tempfile
 
+import configs_test
 import utils
 from wvtest import wvtest
 
@@ -18,8 +18,9 @@
     {'band': '5'},
     {'width': '40'},
     {'autotype': 'ANY'},
-    {'protocols': ('a', 'b', 'ac')},
+    {'protocols': 'a/b/ac'},
     {'encryption': 'NONE'},
+    {'wds': True, 'bridge': 'br0'},
 )
 
 _VALIDATION_FAIL = (
@@ -32,25 +33,23 @@
     {'band': '2.4', 'autotype': 'DFS'},
     {'band': '5', 'autotype': 'OVERLAP'},
     # Invalid protocols
-    {'protocols': set('abc')},
-    {'protocols': set()},
+    {'protocols': 'a/b/c'},
+    {'protocols': ''},
     # Invalid width
     {'width': '25'},
     # Invalid width/protocols
-    {'width': '40', 'protocols': set('abg')},
-    {'width': '80', 'protocols': set('abgn')},
+    {'width': '40', 'protocols': 'a/b/g'},
+    {'width': '80', 'protocols': 'a/b/g/n'},
+    {'wds': True, 'bridge': ''},
 )
 
 
-_DEFAULTS = collections.OrderedDict((('band', '2.4'), ('width', '20'),
-                                     ('autotype', 'NONDFS'),
-                                     ('protocols', ('a', 'b', 'g', 'n', 'ac')),
-                                     ('encryption', 'WPA2_PSK_AES')))
-
-
-def modify_defaults(**kwargs):
-  result = collections.OrderedDict(_DEFAULTS)
-  result.update(kwargs)
+def make_optdict(**kwargs):
+  result = configs_test.FakeOptDict()
+  # This is the default band for 'wifi set'.
+  result.band = '2.4'
+  for k, v in kwargs.iteritems():
+    setattr(result, k, v)
   return result
 
 
@@ -62,23 +61,24 @@
 
   for case in _VALIDATION_PASS:
     try:
-      utils.validate_set_wifi_options(*modify_defaults(**case).values())
+      utils.validate_set_wifi_options(make_optdict(**case))
+      wvtest.WVPASS(True)  # Make WvTest count this as a test.
     except utils.BinWifiException:
       wvtest.WVFAIL('Test failed.')
 
   for case in _VALIDATION_FAIL:
     wvtest.WVEXCEPT(
         utils.BinWifiException, utils.validate_set_wifi_options,
-        *modify_defaults(**case).values())
+        make_optdict(**case))
 
   # Test failure when WIFI_PSK is missing
   del os.environ['WIFI_PSK']
   wvtest.WVEXCEPT(
       utils.BinWifiException, utils.validate_set_wifi_options,
-      *_DEFAULTS.values())
+      make_optdict(**_VALIDATION_PASS[0]))
   wvtest.WVEXCEPT(
       utils.BinWifiException, utils.validate_set_wifi_options,
-      *modify_defaults(encryption='WEP').values())
+      make_optdict(encryption='WEP'))
 
 
 @wvtest.wvtest
diff --git a/wifi/wifi.py b/wifi/wifi.py
index 42ed53c..e4142c1 100755
--- a/wifi/wifi.py
+++ b/wifi/wifi.py
@@ -27,7 +27,7 @@
 
 _OPTSPEC_FORMAT = """
 {bin} set           Enable or modify access points.  Takes all options unless otherwise specified.
-{bin} setclient     Enable or modify wifi clients.  Takes -b, -P, -s, --bssid, -S.
+{bin} setclient     Enable or modify wifi clients.  Takes -b, -P, -s, --bssid, -S, --wds.
 {bin} stop|off      Disable access points and clients.  Takes -b, -P, -S.
 {bin} stopap        Disable access points.  Takes -b, -P, -S.
 {bin} stopclient    Disable wifi clients.  Takes -b, -P, -S.
@@ -53,6 +53,7 @@
 Y,yottasecond-timeouts            Don't rotate any keys: PTK, GTK, or GMK
 P,persist                         For set commands, persist options so we can restore them with 'wifi restore'.  For stop commands, remove persisted options.
 S,interface-suffix=               Interface suffix (defaults to ALL for stop commands; use NONE to specify no suffix) []
+W,wds                             Enable WDS mode (nl80211 only)
 lock-timeout=                     How long, in seconds, to wait for another /bin/wifi process to finish before giving up. [60]
 scan-ap-force                     (Scan only) scan when in AP mode
 scan-passive                      (Scan only) do not probe, scan passively
@@ -228,8 +229,7 @@
   autotype = opt.autotype
   protocols = set(opt.protocols.split('/'))
 
-  utils.validate_set_wifi_options(
-      band, width, autotype, protocols, opt.encryption)
+  utils.validate_set_wifi_options(opt)
 
   psk = None
   if opt.encryption == 'WEP' or '_PSK_' in opt.encryption:
@@ -840,6 +840,10 @@
   if not _stop_hostapd(interface):
     raise utils.BinWifiException("Couldn't stop hostapd")
 
+  # Set or unset 4-address mode.  This has to be done while hostapd is down.
+  utils.log('%s 4-address mode', 'Enabling' if opt.wds else 'Disabling')
+  iw.set_4address_mode(interface, opt.wds)
+
   # We don't want to try to rewrite this file if this is just a forced restart.
   if not forced:
     utils.atomic_write(tmp_config_filename, config)