Merge "platform: Added ability to add/remove hnvram vars"
diff --git a/conman/connection_manager.py b/conman/connection_manager.py
index 040e812..3808458 100755
--- a/conman/connection_manager.py
+++ b/conman/connection_manager.py
@@ -170,6 +170,7 @@
       subprocess.check_output(command, stderr=subprocess.STDOUT, env=env)
     except subprocess.CalledProcessError as e:
       logging.error('Failed to start wifi client: %s', e.output)
+      self._status.wlan_failed = True
       return False
 
     return True
@@ -212,6 +213,7 @@
   COMMAND_FILE_REGEXP = WLAN_FILE_REGEXP_FMT % COMMAND_FILE_PREFIX
   ACCESS_POINT_FILE_REGEXP = WLAN_FILE_REGEXP_FMT % ACCESS_POINT_FILE_PREFIX
   GATEWAY_FILE_PREFIX = 'gateway.'
+  SUBNET_FILE_PREFIX = 'subnet.'
   MOCA_NODE_FILE_PREFIX = 'node'
   WIFI_SETCLIENT = ['wifi', 'setclient']
   IFUP = ['ifup']
@@ -295,6 +297,7 @@
               self._wpa_control_interface)
 
     for path, prefix in ((self._tmp_dir, self.GATEWAY_FILE_PREFIX),
+                         (self._tmp_dir, self.SUBNET_FILE_PREFIX),
                          (self._interface_status_dir, ''),
                          (self._moca_tmp_dir, self.MOCA_NODE_FILE_PREFIX),
                          (self._config_dir, self.COMMAND_FILE_PREFIX)):
@@ -583,7 +586,7 @@
     """Update the contents of /tmp/hosts."""
     lowest_metric_interface = None
     for ifc in [self.bridge] + self.wifi:
-      route = ifc.current_route()
+      route = ifc.current_routes().get('default', None)
       if route:
         metric = route.get('metric', 0)
         # Skip temporary connection_check routes.
@@ -689,6 +692,14 @@
           logging.info('Received gateway %r for interface %s', contents,
                        ifc.name)
 
+      if filename.startswith(self.SUBNET_FILE_PREFIX):
+        interface_name = filename.split(self.SUBNET_FILE_PREFIX)[-1]
+        ifc = self.interface_by_name(interface_name)
+        if ifc:
+          ifc.set_subnet(contents)
+          logging.info('Received subnet %r for interface %s', contents,
+                       ifc.name)
+
     elif path == self._moca_tmp_dir:
       match = re.match(r'^%s\d+$' % self.MOCA_NODE_FILE_PREFIX, filename)
       if match:
diff --git a/conman/connection_manager_test.py b/conman/connection_manager_test.py
index 1f90f96..271cac7 100755
--- a/conman/connection_manager_test.py
+++ b/conman/connection_manager_test.py
@@ -208,11 +208,12 @@
       #
       # 1)  Write an interface status file.
       # 2)  Call run-dhclient, which would call dhclient-script, which would
-      #     write a gateway file.
+      #     call ipapply, which would write gateway and subnet files.
       #
       # Fake both of these things instead.
       self.write_interface_status_file('1')
       self.write_gateway_file()
+      self.write_subnet_file()
 
   def stop_client(self):
     client_was_up = self.client_up
@@ -233,6 +234,13 @@
       # This value doesn't matter to conman, so it's fine to hard code it here.
       f.write('192.168.1.1')
 
+  def write_subnet_file(self):
+    subnet_file = os.path.join(self.tmp_dir,
+                               self.subnet_file_prefix + self.wifi.name)
+    with open(subnet_file, 'w') as f:
+      # This value doesn't matter to conman, so it's fine to hard code it here.
+      f.write('192.168.1.0/24')
+
   def write_interface_status_file(self, value):
     status_file = os.path.join(self.interface_status_dir, self.wifi.name)
     with open(status_file, 'w') as f:
@@ -374,6 +382,7 @@
     wlan_configuration.tmp_dir = self._tmp_dir
     wlan_configuration.interface_status_dir = self._interface_status_dir
     wlan_configuration.gateway_file_prefix = self.GATEWAY_FILE_PREFIX
+    wlan_configuration.subnet_file_prefix = self.SUBNET_FILE_PREFIX
 
     super(ConnectionManager, self)._update_wlan_configuration(
         wlan_configuration)
@@ -394,6 +403,8 @@
     if up and not dhcp_failure:
       self.write_gateway_file('br0' if interface_name in ('eth0', 'moca0')
                               else interface_name)
+      self.write_subnet_file('br0' if interface_name in ('eth0', 'moca0')
+                             else interface_name)
 
   def _binwifi(self, *command):
     super(ConnectionManager, self)._binwifi(*command)
@@ -438,6 +449,13 @@
       # This value doesn't matter to conman, so it's fine to hard code it here.
       f.write('192.168.1.1')
 
+  def write_subnet_file(self, interface_name):
+    subnet_file = os.path.join(self._tmp_dir,
+                               self.SUBNET_FILE_PREFIX + interface_name)
+    with open(subnet_file, 'w') as f:
+      # This value doesn't matter to conman, so it's fine to hard code it here.
+      f.write('192.168.1.0/24')
+
   def write_interface_status_file(self, interface_name, value):
     status_file = os.path.join(self._interface_status_dir, interface_name)
     with open(status_file, 'w') as f:
@@ -616,10 +634,10 @@
   c.run_once()
   wvtest.WVPASS(c.acs())
   wvtest.WVPASS(c.internet())
-  wvtest.WVPASS(c.bridge.current_route())
+  wvtest.WVPASS(c.bridge.current_routes())
   wvtest.WVPASS(os.path.exists(acs_autoprov_filepath))
   for wifi in c.wifi:
-    wvtest.WVFAIL(wifi.current_route())
+    wvtest.WVFAIL(wifi.current_routes_normal_testonly())
   wvtest.WVFAIL(c.has_status_files([status.P.CONNECTED_TO_WLAN,
                                     status.P.HAVE_CONFIG]))
 
@@ -628,7 +646,7 @@
   c.run_once()
   wvtest.WVFAIL(c.acs())
   wvtest.WVFAIL(c.internet())
-  wvtest.WVFAIL(c.bridge.current_route())
+  wvtest.WVFAIL(c.bridge.current_routes_normal_testonly())
   wvtest.WVFAIL(os.path.exists(acs_autoprov_filepath))
   wvtest.WVFAIL(c.has_status_files([status.P.CAN_REACH_ACS,
                                     status.P.CAN_REACH_INTERNET]))
@@ -638,35 +656,35 @@
   c.run_once()
   wvtest.WVPASS(c.acs())
   wvtest.WVPASS(c.internet())
-  wvtest.WVPASS(c.bridge.current_route())
+  wvtest.WVPASS(c.bridge.current_routes())
 
   # Bring up ethernet, access via both moca and ethernet.
   c.set_ethernet(True)
   c.run_once()
   wvtest.WVPASS(c.acs())
   wvtest.WVPASS(c.internet())
-  wvtest.WVPASS(c.bridge.current_route())
+  wvtest.WVPASS(c.bridge.current_routes())
 
   # Bring down moca, still have access via ethernet.
   c.set_moca(False)
   c.run_once()
   wvtest.WVPASS(c.acs())
   wvtest.WVPASS(c.internet())
-  wvtest.WVPASS(c.bridge.current_route())
+  wvtest.WVPASS(c.bridge.current_routes())
 
   # The bridge interfaces are up, but they can't reach anything.
   c.bridge.set_connection_check_result('fail')
   c.run_until_interface_update()
   wvtest.WVFAIL(c.acs())
   wvtest.WVFAIL(c.internet())
-  wvtest.WVFAIL(c.bridge.current_route())
+  wvtest.WVFAIL(c.bridge.current_routes_normal_testonly())
 
   # Now c connects to a restricted network.
   c.bridge.set_connection_check_result('restricted')
   c.run_until_interface_update()
   wvtest.WVPASS(c.acs())
   wvtest.WVFAIL(c.internet())
-  wvtest.WVPASS(c.bridge.current_route())
+  wvtest.WVPASS(c.bridge.current_routes())
 
   # Now the wired connection goes away.
   c.set_ethernet(False)
@@ -674,7 +692,9 @@
   c.run_once()
   wvtest.WVFAIL(c.acs())
   wvtest.WVFAIL(c.internet())
-  wvtest.WVFAIL(c.bridge.current_route())
+  # We have no links, so we should have no routes (not even low priority ones),
+  # and /tmp/hosts should only contain a line for localhost.
+  wvtest.WVFAIL(c.bridge.current_routes())
   check_tmp_hosts('127.0.0.1 localhost')
 
   # Now there are some scan results.
@@ -696,7 +716,7 @@
   wvtest.WVPASS(c.acs())
   wvtest.WVPASS(c.internet())
   wvtest.WVFAIL(c.client_up(band))
-  wvtest.WVPASS(c.wifi_for_band(band).current_route())
+  wvtest.WVPASS(c.wifi_for_band(band).current_routes())
   wvtest.WVPASSEQ(c.log_upload_count, 1)
   # Disable scan results again.
   c.interface_with_scan_results = None
@@ -710,7 +730,7 @@
   c.disable_access_point(band)
   c.run_once()
   wvtest.WVPASS(c.client_up(band))
-  wvtest.WVPASS(c.wifi_for_band(band).current_route())
+  wvtest.WVPASS(c.wifi_for_band(band).current_routes())
   wvtest.WVPASS(c.has_status_files([status.P.CONNECTED_TO_WLAN]))
 
   # Kill wpa_supplicant.  conman should restart it.
@@ -751,7 +771,7 @@
   c.disable_access_point(band)
   c.run_once()
   wvtest.WVPASS(c.client_up(band))
-  wvtest.WVPASS(c.wifi_for_band(band).current_route())
+  wvtest.WVPASS(c.wifi_for_band(band).current_routes())
   wvtest.WVPASS(c.has_status_files([status.P.CONNECTED_TO_WLAN]))
 
   # Now enable the AP.  Since we have no wired connection, this should have no
@@ -759,8 +779,8 @@
   c.enable_access_point(band)
   c.run_once()
   wvtest.WVPASS(c.client_up(band))
-  wvtest.WVPASS(c.wifi_for_band(band).current_route())
-  wvtest.WVFAIL(c.bridge.current_route())
+  wvtest.WVPASS(c.wifi_for_band(band).current_routes())
+  wvtest.WVFAIL(c.bridge.current_routes_normal_testonly())
   c.run_until_interface_update()
   check_tmp_hosts('192.168.1.100 %s\n127.0.0.1 localhost' % hostname)
 
@@ -772,8 +792,8 @@
   c.run_until_interface_update()
   wvtest.WVPASS(c.access_point_up(band))
   wvtest.WVFAIL(c.client_up(band))
-  wvtest.WVFAIL(c.wifi_for_band(band).current_route())
-  wvtest.WVPASS(c.bridge.current_route())
+  wvtest.WVFAIL(c.wifi_for_band(band).current_routes())
+  wvtest.WVPASS(c.bridge.current_routes())
   check_tmp_hosts('192.168.1.101 %s\n127.0.0.1 localhost' % hostname)
 
   # Now move (rather than delete) the configuration file.  The AP should go
@@ -785,8 +805,8 @@
   c.run_once()
   wvtest.WVFAIL(c.access_point_up(band))
   wvtest.WVFAIL(c.client_up(band))
-  wvtest.WVFAIL(c.wifi_for_band(band).current_route())
-  wvtest.WVPASS(c.bridge.current_route())
+  wvtest.WVFAIL(c.wifi_for_band(band).current_routes_normal_testonly())
+  wvtest.WVPASS(c.bridge.current_routes())
   wvtest.WVFAIL(c.has_status_files([status.P.HAVE_CONFIG]))
 
   # Now move it back, and the AP should come back.
@@ -794,8 +814,8 @@
   c.run_once()
   wvtest.WVPASS(c.access_point_up(band))
   wvtest.WVFAIL(c.client_up(band))
-  wvtest.WVFAIL(c.wifi_for_band(band).current_route())
-  wvtest.WVPASS(c.bridge.current_route())
+  wvtest.WVFAIL(c.wifi_for_band(band).current_routes_normal_testonly())
+  wvtest.WVPASS(c.bridge.current_routes())
 
   # Now delete the config and bring down the bridge and make sure we reprovision
   # via the last working BSS.
@@ -805,7 +825,9 @@
   c.run_until_interface_update()
   wvtest.WVFAIL(c.acs())
   wvtest.WVFAIL(c.internet())
-  check_tmp_hosts('127.0.0.1 localhost')
+  # We still have a link and might be wrong about the connection_check, so
+  # /tmp/hosts should still contain a line for this hostname.
+  check_tmp_hosts('192.168.1.101 %s\n127.0.0.1 localhost' % hostname)
   # s3 is not what the cycler would suggest trying next.
   wvtest.WVPASSNE('s3', c.wifi_for_band(band).cycler.peek())
   # Run only once, so that only one BSS can be tried.  It should be the s3 one,
@@ -979,11 +1001,11 @@
   c.run_once()
   wvtest.WVPASS(c.access_point_up('2.4'))
   wvtest.WVPASS(c.access_point_up('5'))
-  wvtest.WVPASS(c.bridge.current_route())
+  wvtest.WVPASS(c.bridge.current_routes())
   wvtest.WVFAIL(c.client_up('2.4'))
   wvtest.WVFAIL(c.client_up('5'))
-  wvtest.WVFAIL(c.wifi_for_band('2.4').current_route())
-  wvtest.WVFAIL(c.wifi_for_band('5').current_route())
+  wvtest.WVFAIL(c.wifi_for_band('2.4').current_routes_normal_testonly())
+  wvtest.WVFAIL(c.wifi_for_band('5').current_routes_normal_testonly())
 
   # Disable the 2.4 GHz AP, make sure the 5 GHz AP stays up.  2.4 GHz should
   # join the WLAN.
@@ -992,9 +1014,9 @@
   wvtest.WVFAIL(c.access_point_up('2.4'))
   wvtest.WVPASS(c.access_point_up('5'))
   wvtest.WVPASS(c.client_up('2.4'))
-  wvtest.WVPASS(c.bridge.current_route())
-  wvtest.WVPASS(c.wifi_for_band('2.4').current_route())
-  wvtest.WVFAIL(c.wifi_for_band('5').current_route())
+  wvtest.WVPASS(c.bridge.current_routes())
+  wvtest.WVPASS(c.wifi_for_band('2.4').current_routes())
+  wvtest.WVFAIL(c.wifi_for_band('5').current_routes_normal_testonly())
 
   # Delete the 2.4 GHz WLAN configuration; it should leave the WLAN but nothing
   # else should change.
@@ -1003,9 +1025,9 @@
   wvtest.WVFAIL(c.access_point_up('2.4'))
   wvtest.WVPASS(c.access_point_up('5'))
   wvtest.WVFAIL(c.client_up('2.4'))
-  wvtest.WVPASS(c.bridge.current_route())
-  wvtest.WVFAIL(c.wifi_for_band('2.4').current_route())
-  wvtest.WVFAIL(c.wifi_for_band('5').current_route())
+  wvtest.WVPASS(c.bridge.current_routes())
+  wvtest.WVFAIL(c.wifi_for_band('2.4').current_routes_normal_testonly())
+  wvtest.WVFAIL(c.wifi_for_band('5').current_routes_normal_testonly())
 
   # Disable the wired connection and remove the WLAN configurations.  Both
   # radios should scan.  Wait for 5 GHz to scan, then enable scan results for
@@ -1014,18 +1036,18 @@
   c.set_ethernet(False)
   c.run_once()
   wvtest.WVFAIL(c.acs())
-  wvtest.WVFAIL(c.bridge.current_route())
-  wvtest.WVFAIL(c.wifi_for_band('2.4').current_route())
-  wvtest.WVFAIL(c.wifi_for_band('5').current_route())
+  wvtest.WVFAIL(c.bridge.current_routes_normal_testonly())
+  wvtest.WVFAIL(c.wifi_for_band('2.4').current_routes_normal_testonly())
+  wvtest.WVFAIL(c.wifi_for_band('5').current_routes_normal_testonly())
 
   # The 5 GHz scan has no results.
   c.run_until_scan('5')
   c.run_once()
   c.run_until_interface_update()
   wvtest.WVFAIL(c.acs())
-  wvtest.WVFAIL(c.bridge.current_route())
-  wvtest.WVFAIL(c.wifi_for_band('2.4').current_route())
-  wvtest.WVFAIL(c.wifi_for_band('5').current_route())
+  wvtest.WVFAIL(c.bridge.current_routes_normal_testonly())
+  wvtest.WVFAIL(c.wifi_for_band('2.4').current_routes_normal_testonly())
+  wvtest.WVFAIL(c.wifi_for_band('5').current_routes_normal_testonly())
 
   # The next 2.4 GHz scan will have results.
   c.interface_with_scan_results = c.wifi_for_band('2.4').name
@@ -1035,9 +1057,9 @@
     c.run_once()
   c.run_until_interface_update()
   wvtest.WVPASS(c.acs())
-  wvtest.WVFAIL(c.bridge.current_route())
-  wvtest.WVPASS(c.wifi_for_band('2.4').current_route())
-  wvtest.WVFAIL(c.wifi_for_band('5').current_route())
+  wvtest.WVFAIL(c.bridge.current_routes_normal_testonly())
+  wvtest.WVPASS(c.wifi_for_band('2.4').current_routes())
+  wvtest.WVFAIL(c.wifi_for_band('5').current_routes_normal_testonly())
   c.run_once()
   wvtest.WVPASSEQ(c.log_upload_count, 1)
 
@@ -1085,9 +1107,9 @@
   c.run_once()
   wvtest.WVFAIL(c.access_point_up('2.4'))
   wvtest.WVPASS(c.access_point_up('5'))
-  wvtest.WVPASS(c.bridge.current_route())
-  wvtest.WVFAIL(c.wifi_for_band('2.4').current_route())
-  wvtest.WVFAIL(c.wifi_for_band('5').current_route())
+  wvtest.WVPASS(c.bridge.current_routes())
+  wvtest.WVFAIL(c.wifi_for_band('2.4').current_routes_normal_testonly())
+  wvtest.WVFAIL(c.wifi_for_band('5').current_routes_normal_testonly())
 
   # Disable the 2.4 GHz AP; nothing should change.  The 2.4 GHz client should
   # not be up because the same radio is being used to run a 5 GHz AP.
@@ -1096,9 +1118,9 @@
   wvtest.WVFAIL(c.access_point_up('2.4'))
   wvtest.WVPASS(c.access_point_up('5'))
   wvtest.WVFAIL(c.client_up('2.4'))
-  wvtest.WVPASS(c.bridge.current_route())
-  wvtest.WVFAIL(c.wifi_for_band('2.4').current_route())
-  wvtest.WVFAIL(c.wifi_for_band('5').current_route())
+  wvtest.WVPASS(c.bridge.current_routes())
+  wvtest.WVFAIL(c.wifi_for_band('2.4').current_routes_normal_testonly())
+  wvtest.WVFAIL(c.wifi_for_band('5').current_routes_normal_testonly())
 
   # Delete the 2.4 GHz WLAN configuration; nothing should change.
   c.delete_wlan_config('2.4')
@@ -1106,9 +1128,9 @@
   wvtest.WVFAIL(c.access_point_up('2.4'))
   wvtest.WVPASS(c.access_point_up('5'))
   wvtest.WVFAIL(c.client_up('2.4'))
-  wvtest.WVPASS(c.bridge.current_route())
-  wvtest.WVFAIL(c.wifi_for_band('2.4').current_route())
-  wvtest.WVFAIL(c.wifi_for_band('5').current_route())
+  wvtest.WVPASS(c.bridge.current_routes())
+  wvtest.WVFAIL(c.wifi_for_band('2.4').current_routes_normal_testonly())
+  wvtest.WVFAIL(c.wifi_for_band('5').current_routes_normal_testonly())
 
   # Disable the wired connection and remove the WLAN configurations.  There
   # should be a single scan that leads to ACS access.  (It doesn't matter which
@@ -1118,9 +1140,9 @@
   c.set_ethernet(False)
   c.run_once()
   wvtest.WVFAIL(c.acs())
-  wvtest.WVFAIL(c.bridge.current_route())
-  wvtest.WVFAIL(c.wifi_for_band('2.4').current_route())
-  wvtest.WVFAIL(c.wifi_for_band('5').current_route())
+  wvtest.WVFAIL(c.bridge.current_routes_normal_testonly())
+  wvtest.WVFAIL(c.wifi_for_band('2.4').current_routes_normal_testonly())
+  wvtest.WVFAIL(c.wifi_for_band('5').current_routes_normal_testonly())
 
   # The scan will have results that will lead to ACS access.
   c.interface_with_scan_results = c.wifi_for_band('2.4').name
@@ -1129,9 +1151,9 @@
     c.run_once()
   c.run_until_interface_update()
   wvtest.WVPASS(c.acs())
-  wvtest.WVFAIL(c.bridge.current_route())
-  wvtest.WVPASS(c.wifi_for_band('2.4').current_route())
-  wvtest.WVPASS(c.wifi_for_band('5').current_route())
+  wvtest.WVFAIL(c.bridge.current_routes_normal_testonly())
+  wvtest.WVPASS(c.wifi_for_band('2.4').current_routes())
+  wvtest.WVPASS(c.wifi_for_band('5').current_routes())
   c.run_once()
   wvtest.WVPASSEQ(c.log_upload_count, 1)
 
@@ -1176,7 +1198,7 @@
   c.run_once()
   wvtest.WVPASS(c.wifi_for_band('2.4').acs())
   wvtest.WVPASS(c.wifi_for_band('2.4').internet())
-  wvtest.WVPASS(c.wifi_for_band('2.4').current_route())
+  wvtest.WVPASS(c.wifi_for_band('2.4').current_routes())
 
 
 @wvtest.wvtest
@@ -1189,7 +1211,7 @@
     c:  The ConnectionManager set up by @connection_manager_test.
   """
   wvtest.WVPASS(c._connected_to_wlan(c.wifi_for_band('2.4')))
-  wvtest.WVPASS(c.wifi_for_band('2.4').current_route)
+  wvtest.WVPASS(c.wifi_for_band('2.4').current_routes())
 
 
 @wvtest.wvtest
diff --git a/conman/interface.py b/conman/interface.py
index 707890b..cd4d323 100755
--- a/conman/interface.py
+++ b/conman/interface.py
@@ -7,6 +7,10 @@
 import re
 import subprocess
 
+# This has to be called before another module calls it with a higher log level.
+# pylint: disable=g-import-not-at-top
+logging.basicConfig(level=logging.DEBUG)
+
 import experiment
 import wpactrl
 
@@ -15,6 +19,8 @@
 METRIC_24GHZ = 22
 METRIC_TEMPORARY_CONNECTION_CHECK = 99
 
+RFC2385_MULTICAST_ROUTE = '239.0.0.0/8'
+
 experiment.register('WifiSimulateWireless')
 CWMP_PATH = '/tmp/cwmp'
 MAX_ACS_FAILURE_S = 60
@@ -30,7 +36,7 @@
   IP_ROUTE = ['ip', 'route']
   IP_ADDR_SHOW = ['ip', 'addr', 'show', 'dev']
 
-  def __init__(self, name, metric):
+  def __init__(self, name, base_metric):
     self.name = name
 
     # Currently connected links for this interface, e.g. ethernet.
@@ -40,13 +46,18 @@
     self._has_acs = None
     self._has_internet = None
 
-    # The gateway IP for this interface.
+    self._subnet = None
     self._gateway_ip = None
-    self.metric = metric
+    self.base_metric = base_metric
+    self.metric_offset = 0
 
     # Until this is set True, the routing table will not be touched.
     self._initialized = False
 
+  @property
+  def metric(self):
+    return str(int(self.base_metric) + self.metric_offset)
+
   def _connection_check(self, check_acs):
     """Check this interface's connection status.
 
@@ -76,9 +87,12 @@
     # Give it a high metric so that it won't interfere with normal default
     # routes.
     added_temporary_route = False
-    if not self.current_route():
-      logging.debug('Adding temporary connection check route for dev %s',
+    if 'default' not in self.current_routes():
+      logging.debug('Adding temporary connection check routes for dev %s',
                     self.name)
+      self._ip_route('add', self._gateway_ip,
+                     'dev', self.name,
+                     'metric', str(METRIC_TEMPORARY_CONNECTION_CHECK))
       self._ip_route('add', 'default',
                      'via', self._gateway_ip,
                      'dev', self.name,
@@ -98,11 +112,14 @@
 
     # Delete the temporary route.
     if added_temporary_route:
-      logging.debug('Deleting temporary connection check route for dev %s',
+      logging.debug('Deleting temporary connection check routes for dev %s',
                     self.name)
       self._ip_route('del', 'default',
                      'dev', self.name,
                      'metric', str(METRIC_TEMPORARY_CONNECTION_CHECK))
+      self._ip_route('del', self._gateway_ip,
+                     'dev', self.name,
+                     'metric', str(METRIC_TEMPORARY_CONNECTION_CHECK))
 
     return result
 
@@ -121,11 +138,10 @@
 
     return self._has_internet
 
-  def add_route(self):
-    """Adds a default route for this interface.
+  def add_routes(self):
+    """Update default routes for this interface.
 
-    First, checks whether an equivalent route already exists, and if so,
-    returns.
+    Remove any stale routes and add any missing desired routes.
     """
     if self.metric is None:
       logging.info('Cannot add route for %s without a metric.', self.name)
@@ -135,45 +151,88 @@
       logging.info('Cannot add route for %s without a gateway IP.', self.name)
       return
 
-    # If the current default route is the same, there is nothing to do.  If it
+    # If the current routes are the same, there is nothing to do.  If either
     # exists but is different, delete it before adding an updated one.
-    current = self.current_route()
-    if current:
-      if (current.get('via', None) == self._gateway_ip and
-          current.get('metric', None) == str(self.metric)):
-        return
-      else:
-        self.delete_route()
+    current = self.current_routes()
+    default = current.get('default', {})
+    if ((default.get('via', None), default.get('metric', None)) !=
+        (self._gateway_ip, str(self.metric))):
+      logging.debug('Adding default route for dev %s', self.name)
+      self.delete_route('default')
+      self._ip_route('add', 'default',
+                     'via', self._gateway_ip,
+                     'dev', self.name,
+                     'metric', str(self.metric))
 
-    logging.debug('Adding default route for dev %s', self.name)
-    self._ip_route('add', 'default',
-                   'via', self._gateway_ip,
-                   'dev', self.name,
-                   'metric', str(self.metric))
+    subnet = current.get('subnet', {})
+    if (self._subnet and
+        (subnet.get('via', None), subnet.get('metric', None)) !=
+        (self._gateway_ip, str(self.metric))):
+      logging.debug('Adding subnet route for dev %s', self.name)
+      self.delete_route('subnet')
+      self._ip_route('add', self._subnet,
+                     'dev', self.name,
+                     'metric', str(self.metric))
 
-  def delete_route(self):
-    while self.current_route():
-      logging.debug('Deleting default route for dev %s', self.name)
-      self._ip_route('del', 'default',
-                     'dev', self.name)
+    # RFC2365 multicast route.
+    if current.get('multicast', {}).get('metric', None) != str(self.metric):
+      logging.debug('Adding multicast route for dev %s', self.name)
+      self.delete_route('multicast')
+      self._ip_route('add', RFC2385_MULTICAST_ROUTE,
+                     'dev', self.name,
+                     'metric', str(self.metric))
 
-  def current_route(self):
-    """Read the current default route for this interface.
+  def delete_route(self, *args):
+    """Delete default and/or subnet routes for this interface.
+
+    Args:
+      *args:  Which routes to delete.  Must be at least one of 'default',
+          'subnet', 'multicast'.
+
+    Raises:
+      ValueError:  If neither default nor subnet is True.
+    """
+    args = set(args)
+    args &= set(('default', 'subnet', 'multicast'))
+    if not args:
+      raise ValueError(
+          'Must specify at least one of default, subnet, multicast to delete.')
+
+    for route_type in args:
+      while route_type in self.current_routes():
+        logging.debug('Deleting %s route for dev %s', route_type, self.name)
+        self._ip_route('del', self.current_routes()[route_type]['route'],
+                       'dev', self.name)
+
+  def current_routes(self):
+    """Read the current routes for this interface.
 
     Returns:
-      A dict containing the gateway [and metric] of the route, or an empty dict
-      if there is currently no default route for this interface.
+      A dict mapping 'default' and/or 'subnet' to a dict containing the gateway
+      [and metric] of the route.  Only contains keys for routes that are
+      present.
     """
     result = {}
     for line in self._ip_route().splitlines():
-      if line.startswith('default') and 'dev %s' % self.name in line:
-        key = None
+      if 'dev %s' % self.name in line:
+        if line.startswith('default'):
+          route_type = 'default'
+        elif re.search(r'/\d{1,2}$', line.split()[0]):
+          route_type = 'subnet'
+        else:
+          continue
+        route = {}
+        key = 'route'
         for token in line.split():
           if token in ['via', 'metric']:
             key = token
           elif key:
-            result[key] = token
+            if key == 'route' and token == RFC2385_MULTICAST_ROUTE:
+              route_type = 'multicast'
+            route[key] = token
             key = None
+        if route:
+          result[route_type] = route
 
     return result
 
@@ -209,7 +268,12 @@
   def set_gateway_ip(self, gateway_ip):
     logging.info('New gateway IP %s for %s', gateway_ip, self.name)
     self._gateway_ip = gateway_ip
-    self.update_routes()
+    self.update_routes(expire_cache=True)
+
+  def set_subnet(self, subnet):
+    logging.info('New subnet %s for %s', subnet, self.name)
+    self._subnet = subnet
+    self.update_routes(expire_cache=True)
 
   def _set_link_status(self, link, is_up):
     """Set whether a link is up or not."""
@@ -243,36 +307,51 @@
   def update_routes(self, expire_cache=True):
     """Update this interface's routes.
 
-    If the interface has gained ACS or internet access, add a route.  If it had
-    either and now has neither, delete the route.
+    If the interface has ACS or internet access, prioritize its routes.  If it
+    doesn't but has a link, deprioritize the routes.  If it has no links, delete
+    the routes.
 
     Args:
       expire_cache:  If true, force a recheck of connection status before
-      deciding whether to add or remove routes.
+      deciding how to prioritize routes.
     """
     logging.debug('Updating routes for %s', self.name)
-    maybe_had_acs = self._has_acs
-    maybe_had_internet = self._has_internet
-
     if expire_cache:
       self.expire_connection_status_cache()
 
-    has_acs = self.acs()
-    has_internet = self.internet()
+    if self.acs() or self.internet():
+      self.prioritize_routes()
+    else:
+      # If we still have a link, just deprioritize the routes, in case we're
+      # wrong about the connection check.  If there's no actual link, then
+      # really delete the routes.
+      if self.links:
+        self.deprioritize_routes()
+      else:
+        self.delete_route('default', 'subnet', 'multicast')
 
-    # This is a little confusing:  We want to try adding a route if we _may_
-    # have gone from no access to some access, and we want to try deleting the
-    # route if we _may_ have lost *all* access. So the first condition checks
-    # for truthiness but the elif checks for explicit Falsity (i.e. excluding
-    # the None/unknown case).
-    had_access = maybe_had_acs or maybe_had_internet
-    # pylint: disable=g-explicit-bool-comparison
-    maybe_had_access = maybe_had_acs != False or maybe_had_internet != False
-    has_access = has_acs or has_internet
-    if not had_access and has_access:
-      self.add_route()
-    elif maybe_had_access and not has_access:
-      self.delete_route()
+  def prioritize_routes(self):
+    """When connection check succeeds, route priority (metric) should be normal.
+
+    This is the inverse of deprioritize_routes.
+    """
+    if not self._initialized:
+      return
+    logging.info('%s routes have normal priority', self.name)
+    self.metric_offset = 0
+    self.add_routes()
+
+  def deprioritize_routes(self):
+    """When connection check fails, deprioritize routes by increasing metric.
+
+    This is conservative alternative to deleting routes, in case we are mistaken
+    about route not providing a useful connection.
+    """
+    if not self._initialized:
+      return
+    logging.info('%s routes have low priority', self.name)
+    self.metric_offset = 50
+    self.add_routes()
 
   def initialize(self):
     """Tell the interface it has its initial state.
@@ -319,16 +398,22 @@
       self._moca_stations.remove(node_id)
       self.moca = bool(self._moca_stations)
 
-  def add_route(self):
+  def prioritize_routes(self):
     """We only want ACS autoprovisioning when we're using a wired route."""
-    super(Bridge, self).add_route()
+    super(Bridge, self).prioritize_routes()
     open(self._acs_autoprovisioning_filepath, 'w')
 
-  def delete_route(self):
+  def deprioritize_routes(self, *args, **kwargs):
     """We only want ACS autoprovisioning when we're using a wired route."""
     if os.path.exists(self._acs_autoprovisioning_filepath):
       os.unlink(self._acs_autoprovisioning_filepath)
-    super(Bridge, self).delete_route()
+    super(Bridge, self).deprioritize_routes(*args, **kwargs)
+
+  def delete_route(self, *args, **kwargs):
+    """We only want ACS autoprovisioning when we're using a wired route."""
+    if os.path.exists(self._acs_autoprovisioning_filepath):
+      os.unlink(self._acs_autoprovisioning_filepath)
+    super(Bridge, self).delete_route(*args, **kwargs)
 
   def _connection_check(self, check_acs):
     """Support for WifiSimulateWireless."""
diff --git a/conman/interface_test.py b/conman/interface_test.py
index 13dcf14..e8ab4ca 100755
--- a/conman/interface_test.py
+++ b/conman/interface_test.py
@@ -48,14 +48,18 @@
   def _really_ip_route(self, *args):
     if not args:
       return '\n'.join(self.routing_table.values() +
-                       ['1.2.3.4/24 dev %s proto kernel scope link' % self.name,
+                       ['1.2.3.4/24 dev fake0 proto kernel scope link',
+                        # Non-subnet route, e.g. to NFS host.
+                        '1.2.3.1 dev %s proto kernel scope link' % self.name,
                         'default via 1.2.3.4 dev fake0',
                         'random junk'])
 
     metric = None
     if 'metric' in args:
       metric = args[args.index('metric') + 1]
-    key = (self.name, metric)
+    if args[0] in ('add', 'del'):
+      route = args[1]
+    key = (self.name, route, metric)
     if args[0] == 'add' and key not in self.routing_table:
       logging.debug('Adding route for %r', key)
       self.routing_table[key] = ' '.join(args[1:])
@@ -63,10 +67,10 @@
       if key in self.routing_table:
         logging.debug('Deleting route for %r', key)
         del self.routing_table[key]
-      elif key[1] is None:
+      elif key[2] is None:
         # pylint: disable=g-builtin-op
         for k in self.routing_table.keys():
-          if k[0] == key[0]:
+          if k[:-1] == key[:-1]:
             logging.debug('Deleting route for %r (generalized from %s)', k, key)
             del self.routing_table[k]
             break
@@ -77,6 +81,10 @@
 
     return ''
 
+  def current_routes_normal_testonly(self):
+    result = self.current_routes()
+    return {k: v for k, v in result.iteritems() if int(v.get('metric', 0)) < 50}
+
 
 class Bridge(FakeInterfaceMixin, interface.Bridge):
   pass
@@ -330,53 +338,65 @@
 
     wvtest.WVFAIL(b.acs())
     wvtest.WVFAIL(b.internet())
-    wvtest.WVFAIL(b.current_route())
+    wvtest.WVFAIL(b.current_routes())
+    wvtest.WVFAIL(b.current_routes_normal_testonly())
     wvtest.WVFAIL(os.path.exists(autoprov_filepath))
 
     b.add_moca_station(0)
+    wvtest.WVFAIL(os.path.exists(autoprov_filepath))
     b.set_gateway_ip('192.168.1.1')
+    b.set_subnet('192.168.1.0/24')
+    wvtest.WVFAIL(os.path.exists(autoprov_filepath))
     # Everything should fail because the interface is not initialized.
     wvtest.WVFAIL(b.acs())
     wvtest.WVFAIL(b.internet())
-    wvtest.WVFAIL(b.current_route())
+    wvtest.WVFAIL(b.current_routes_normal_testonly())
     wvtest.WVFAIL(os.path.exists(autoprov_filepath))
     b.initialize()
     wvtest.WVPASS(b.acs())
     wvtest.WVPASS(b.internet())
-    wvtest.WVPASS(b.current_route())
+    current_routes = b.current_routes()
+    wvtest.WVPASSEQ(len(current_routes), 3)
+    wvtest.WVPASS('default' in current_routes)
+    wvtest.WVPASS('subnet' in current_routes)
+    wvtest.WVPASS('multicast' in current_routes)
     wvtest.WVPASS(os.path.exists(autoprov_filepath))
 
     b.add_moca_station(1)
     wvtest.WVPASS(b.acs())
     wvtest.WVPASS(b.internet())
-    wvtest.WVPASS(b.current_route())
+    wvtest.WVPASSEQ(len(b.current_routes()), 3)
     wvtest.WVPASS(os.path.exists(autoprov_filepath))
 
     b.remove_moca_station(0)
     b.remove_moca_station(1)
     wvtest.WVFAIL(b.acs())
     wvtest.WVFAIL(b.internet())
-    wvtest.WVFAIL(b.current_route())
+    # We have no links, so should have no routes.
+    wvtest.WVFAIL(b.current_routes())
     wvtest.WVFAIL(os.path.exists(autoprov_filepath))
 
     b.add_moca_station(2)
     wvtest.WVPASS(b.acs())
     wvtest.WVPASS(b.internet())
-    wvtest.WVPASS(b.current_route())
+    wvtest.WVPASSEQ(len(b.current_routes()), 3)
     wvtest.WVPASS(os.path.exists(autoprov_filepath))
 
     b.set_connection_check_result('fail')
     b.update_routes()
     wvtest.WVFAIL(b.acs())
     wvtest.WVFAIL(b.internet())
-    wvtest.WVFAIL(b.current_route())
+    # We have links but the connection check failed, so we should only have a
+    # low priority route, i.e. metric at least 50.
+    wvtest.WVPASSEQ(len(b.current_routes()), 3)
+    wvtest.WVFAIL(b.current_routes_normal_testonly())
     wvtest.WVFAIL(os.path.exists(autoprov_filepath))
 
     b.set_connection_check_result('restricted')
     b.update_routes()
     wvtest.WVPASS(b.acs())
     wvtest.WVFAIL(b.internet())
-    wvtest.WVPASS(b.current_route())
+    wvtest.WVPASSEQ(len(b.current_routes()), 3)
     wvtest.WVPASS(os.path.exists(autoprov_filepath))
 
     wvtest.WVFAIL(b.get_ip_address())
diff --git a/conman/status.py b/conman/status.py
index e21dc01..7f75682 100644
--- a/conman/status.py
+++ b/conman/status.py
@@ -20,6 +20,7 @@
 
   TRYING_OPEN = 'TRYING_OPEN'
   TRYING_WLAN = 'TRYING_WLAN'
+  WLAN_FAILED = 'WLAN_FAILED'
   CONNECTED_TO_OPEN = 'CONNECTED_TO_OPEN'
   CONNECTED_TO_WLAN = 'CONNECTED_TO_WLAN'
   HAVE_CONFIG = 'HAVE_CONFIG'
@@ -43,6 +44,10 @@
         (),
         (P.TRYING_OPEN, P.CONNECTED_TO_OPEN, P.CONNECTED_TO_WLAN)
     ),
+    P.WLAN_FAILED: (
+        (),
+        (P.TRYING_WLAN, P.CONNECTED_TO_WLAN)
+    ),
     P.CONNECTED_TO_OPEN: (
         (),
         (P.CONNECTED_TO_WLAN, P.TRYING_OPEN, P.TRYING_WLAN)
diff --git a/ginstall/ginstall.py b/ginstall/ginstall.py
index 5db5493..81be82a 100755
--- a/ginstall/ginstall.py
+++ b/ginstall/ginstall.py
@@ -625,6 +625,14 @@
                 'gftv200-39-pre1 and before.')
 
 
+def CheckMultiLoader(manifest):
+  """Check if this ginstall image supports platform-named loaders."""
+  multiloader = manifest.get('multiloader')
+  if not multiloader:
+    return False
+  return True
+
+
 class ProgressBar(object):
   """Progress bar that prints one dot per 1MB."""
 
@@ -863,6 +871,12 @@
   CheckMinimumVersion(manifest)
   CheckMisc(manifest)
 
+  loader_bin_list = ['loader.img', 'loader.bin']
+  loader_sig_list = ['loader.sig']
+  if CheckMultiLoader(manifest):
+    loader_bin_list = ['loader.%s.bin' % GetPlatform().lower()]
+    loader_sig_list = ['loader.%s.sig' % GetPlatform().lower()]
+
   uloader = loader = None
   uloadersig = FileWithSecureHash(StringIO.StringIO(''), 'badsig')
   loadersig = FileWithSecureHash(StringIO.StringIO(''), 'badsig')
@@ -878,10 +892,10 @@
     elif ti.name.startswith('rootfs.'):
       fh = FileWithSecureHash(tar.extractfile(ti), secure_hash)
       InstallRootfs(fh, partition)
-    elif ti.name in ['loader.img', 'loader.bin']:
+    elif ti.name in loader_bin_list:
       buf = StringIO.StringIO(tar.extractfile(ti).read())
       loader = FileWithSecureHash(buf, secure_hash)
-    elif ti.name == 'loader.sig':
+    elif ti.name in loader_sig_list:
       buf = StringIO.StringIO(tar.extractfile(ti).read())
       loadersig = FileWithSecureHash(buf, secure_hash)
     elif ti.name == 'uloader.img':
@@ -927,7 +941,10 @@
     pass
 
   try:
-    return open(path)
+    if path == '-':
+      return sys.stdin
+    else:
+      return open(path)
   except ValueError:
     pass
 
diff --git a/ginstall/ginstall_test.py b/ginstall/ginstall_test.py
index 8deac64..a4e3c8a 100755
--- a/ginstall/ginstall_test.py
+++ b/ginstall/ginstall_test.py
@@ -21,6 +21,7 @@
 import shutil
 import StringIO
 import struct
+import sys
 import tempfile
 import unittest
 import ginstall
@@ -365,6 +366,21 @@
     total = ginstall.GetMemTotal()
     self.assertTrue(total < 4*1e9)
 
+  def testOpenPathOrUrl(self):
+    # URL
+    two_oh_four = ginstall.OpenPathOrUrl('http://www.gstatic.com/generate_204')
+    self.assertEqual(204, two_oh_four.getcode())
+
+    # on-disk file
+    on_disk_file = tempfile.NamedTemporaryFile()
+    testdata = os.urandom(16)
+    on_disk_file.write(testdata)
+    on_disk_file.flush()
+    self.assertEqual(ginstall.OpenPathOrUrl(on_disk_file.name).read(), testdata)
+
+    # stdin (-)
+    self.assertEqual(ginstall.OpenPathOrUrl('-'), sys.stdin)
+
 
 if __name__ == '__main__':
   unittest.main()
diff --git a/ginstall/install_test.sh b/ginstall/install_test.sh
index 4338bac..ec372c5 100755
--- a/ginstall/install_test.sh
+++ b/ginstall/install_test.sh
@@ -5,6 +5,7 @@
 tmpdir="$(mktemp -d)"
 export PATH="$tmpdir/bin:${PATH}"
 export GINSTALL_OUT_FILE="$tmpdir/out"
+psiz=$(stat --format=%s testdata/img/loader.gflt110.bin)
 lsiz=$(stat --format=%s testdata/img/loader.img)
 ksiz=$(stat --format=%s testdata/img/kernel.img)
 rsiz=$(stat --format=%s testdata/img/rootfs.img)
@@ -195,6 +196,45 @@
 
 
 
+# GFLT110 with gflt110.bin loader.
+echo; echo; echo GFLT110 with platform loader
+setup_fakeroot GFLT110
+expected="\
+psback
+logos ginstall
+flash_unlock ${tmpdir}/dev/mtd6
+flash_erase --quiet ${tmpdir}/dev/mtd6 0 0
+flash_unlock ${tmpdir}/dev/mtd0
+flash_erase --quiet ${tmpdir}/dev/mtd0 0 0
+hnvram -q -w ACTIVATED_KERNEL_NAME=kernel0"
+
+WVPASS ./ginstall.py --basepath="$tmpdir" --tar=./testdata/img/image_gflt110_platform_loader.gi --partition=primary --skiploadersig
+WVPASSEQ "$expected" "$(cat $GINSTALL_OUT_FILE)"
+WVPASS cmp --bytes="$psiz" "${tmpdir}/dev/mtd0" testdata/img/loader.gflt110.bin
+WVPASS cmp --bytes="$ksiz" "${tmpdir}/dev/mtd6" testdata/img/kernel.img
+
+
+
+# GFLT110 with both loaders with the MANIFEST containing "multiloader: 1"
+echo; echo; echo GFLT110 with both loaders
+setup_fakeroot GFLT110
+expected="\
+psback
+logos ginstall
+flash_unlock ${tmpdir}/dev/mtd6
+flash_erase --quiet ${tmpdir}/dev/mtd6 0 0
+flash_unlock ${tmpdir}/dev/mtd0
+flash_erase --quiet ${tmpdir}/dev/mtd0 0 0
+hnvram -q -w ACTIVATED_KERNEL_NAME=kernel0"
+
+WVPASS ./ginstall.py --basepath="$tmpdir" --tar=./testdata/img/image_gflt110_both_loaders.gi --partition=primary --skiploadersig
+WVPASSEQ "$expected" "$(cat $GINSTALL_OUT_FILE)"
+WVFAIL cmp --bytes="$lsiz" "${tmpdir}/dev/mtd0" testdata/img/loader.bin
+WVPASS cmp --bytes="$psiz" "${tmpdir}/dev/mtd0" testdata/img/loader.gflt110.bin
+WVPASS cmp --bytes="$ksiz" "${tmpdir}/dev/mtd6" testdata/img/kernel.img
+
+
+
 echo; echo; echo MANIFEST with Bad checksums
 setup_fakeroot GFHD100
 echo "This should not be touched" >"${tmpdir}/dev/mtd0"
diff --git a/ginstall/testdata/img/image_gflt110_both_loaders.gi b/ginstall/testdata/img/image_gflt110_both_loaders.gi
new file mode 100644
index 0000000..62f2a03
--- /dev/null
+++ b/ginstall/testdata/img/image_gflt110_both_loaders.gi
Binary files differ
diff --git a/ginstall/testdata/img/image_gflt110_platform_loader.gi b/ginstall/testdata/img/image_gflt110_platform_loader.gi
new file mode 100644
index 0000000..8d7ed9b
--- /dev/null
+++ b/ginstall/testdata/img/image_gflt110_platform_loader.gi
Binary files differ
diff --git a/ginstall/testdata/img/loader.gflt110.bin b/ginstall/testdata/img/loader.gflt110.bin
new file mode 100644
index 0000000..a95b6ae
--- /dev/null
+++ b/ginstall/testdata/img/loader.gflt110.bin
@@ -0,0 +1 @@
+gflt110.bin
\ No newline at end of file
diff --git a/signing/S99readallfiles b/signing/S99readallfiles
index 87b97e0..00838fa 100755
--- a/signing/S99readallfiles
+++ b/signing/S99readallfiles
@@ -27,8 +27,11 @@
 case "$1" in
   start)
     (
-      nice -n 19 readallfiles -q / &&
-      clear_failure_count
+      if is-fiberjack; then
+        nice -n 19 readallfiles -q /config && clear_failure_count
+      else
+        nice -n 19 readallfiles -q / && clear_failure_count
+      fi
     ) 2>&1 | logos readall &
     ;;
   stop)
diff --git a/wifi/configs.py b/wifi/configs.py
index 743fe10..97a27ce 100644
--- a/wifi/configs.py
+++ b/wifi/configs.py
@@ -375,7 +375,7 @@
                                utils.validate_and_sanitize_bssid(opt.bssid))
   network_block = make_network_block(network_block_lines)
 
-  freq_list = ','.join(autochannel.get_all_frequencies(opt.band))
+  freq_list = ' '.join(autochannel.get_all_frequencies(opt.band))
 
   lines = [
       'ctrl_interface=/var/run/wpa_supplicant',
diff --git a/wifi/configs_test.py b/wifi/configs_test.py
index 64e05c6..016fc27 100755
--- a/wifi/configs_test.py
+++ b/wifi/configs_test.py
@@ -11,9 +11,9 @@
 
 
 _FREQ_LIST = {
-    '2.4': '2412,2417,2422,2427,2432,2437,2442,2447,2452,2457,2462',
-    '5': ('5180,5200,5220,5240,5745,5765,5785,5805,5825,5260,5280,5300,5320,'
-          '5500,5520,5540,5560,5580,5660,5680,5700'),
+    '2.4': '2412 2417 2422 2427 2432 2437 2442 2447 2452 2457 2462',
+    '5': ('5180 5200 5220 5240 5745 5765 5785 5805 5825 5260 5280 5300 5320 '
+          '5500 5520 5540 5560 5580 5660 5680 5700'),
 }