Merge "conman:  Export status when trying to join WLAN fails."
diff --git a/cmds/Makefile b/cmds/Makefile
index 4f7db2b..24bc8a1 100644
--- a/cmds/Makefile
+++ b/cmds/Makefile
@@ -32,6 +32,7 @@
 	diskbench \
 	dnsck \
 	freemegs \
+	gfhd254_reboot \
 	gstatic \
 	http_bouncer \
 	ionice \
diff --git a/cmds/device_stats.proto b/cmds/device_stats.proto
index 30a344e..4f47b5e 100644
--- a/cmds/device_stats.proto
+++ b/cmds/device_stats.proto
@@ -17,5 +17,8 @@
 
   // Device serial number.
   optional string serial = 5;
+
+  // Public ipv6 address of onu
+  optional string ipv6 = 6;
 };
 
diff --git a/cmds/gfhd254_reboot.c b/cmds/gfhd254_reboot.c
new file mode 100644
index 0000000..fdc7e32
--- /dev/null
+++ b/cmds/gfhd254_reboot.c
@@ -0,0 +1,65 @@
+// GFHD254 has a bug where software reset doesn't reset the entire
+// chip, some state in the SAGE engine isn't getting reset.  This
+// drives a gpio that connects back to the chips own external reset
+// pin, resetting the chip with this pin works around the issue as
+// the SAGE engine is completely reset in this path.
+
+#include <fcntl.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <sys/mman.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+
+#define REG_BASE 0xf0410000
+#define REG_SIZE 0x8000
+
+
+#define GPIO_DATA (0x7404 / 4)
+#define GPIO_IODIR (0x7408 / 4)
+#define CTRL_MUX_0 (0x0700 / 4)
+#define CTRL_MUX_1 (0x0704 / 4)
+
+static void *mmap_(
+    void* addr, size_t size, int prot, int flags, int fd,
+    off_t offset) {
+#ifdef __ANDROID__
+  return mmap64(addr, size, prot, flags, fd,
+                (off64_t)(uint64_t)(uint32_t)offset);
+#else
+  return mmap(addr, size, prot, flags, fd, offset);
+#endif
+}
+
+// TODO(jnewlin):  Revist this after the exact gpio being used
+// is settled on.
+
+int main() {
+  int fd = open("/dev/mem", O_RDWR);
+  volatile uint32_t* reg;
+
+  if (fd < 0) {
+    perror("mmap");
+    return 1;
+  }
+
+  reg = mmap_(NULL, REG_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED,
+              fd, REG_BASE);
+  if (reg == MAP_FAILED) {
+    perror("mmap");
+    return 1;
+  }
+
+  // Set the pin mux to gpio, value of zero selects gpio mode, this
+  // is the reset value so this is probably not required, but just
+  // in case.
+  reg[CTRL_MUX_0] &= ~((0xf << 8) | (0xf << 12)); // aon_gio2 and 3
+  reg[CTRL_MUX_1] &= ~(0xf << 4); // aon_gio9
+
+
+  // Set the direction to be an output and drive it low.
+  reg[GPIO_IODIR] &= ~((1 << 2) | (1 << 3) | (1 << 9));
+  reg[GPIO_DATA] &= ~((1 << 2) | (1 << 3) | (1 << 9));
+
+  return 0;
+}
diff --git a/cmds/statcatcher.cc b/cmds/statcatcher.cc
index 64db2b6..bfd7033 100644
--- a/cmds/statcatcher.cc
+++ b/cmds/statcatcher.cc
@@ -142,7 +142,8 @@
 "onu_acs_contacted": %s,
 "onu_acs_contact_time": "%lld",
 "onu_uptime": %lld,
-"onu_serial": "%s"
+"onu_serial": "%s",
+"onu_ipv6": "%s"
 })";
     FILE *f = fopen(tmp_file.c_str(), "w");
     if (!f) {
@@ -155,7 +156,8 @@
             status.acs_contacted() ? "true" : "false",
             status.acs_contact_time(),
             status.uptime(),
-            status.serial().c_str());
+            status.serial().c_str(),
+            status.ipv6().c_str());
     fclose(f);
 
     if (rename(tmp_file.c_str(), stat_file.c_str()) != 0) {
diff --git a/cmds/statpitcher.cc b/cmds/statpitcher.cc
index 412be5d..990666d 100644
--- a/cmds/statpitcher.cc
+++ b/cmds/statpitcher.cc
@@ -16,8 +16,10 @@
 
 #include <fstream>
 #include <iostream>
+#include <sstream>
 #include <string>
 #include <vector>
+#include <memory>
 
 #include "device_stats.pb.h"
 
@@ -91,6 +93,71 @@
   return static_cast<int64_t>(up);
 }
 
+std::string IPAddress() {
+  std::ifstream infile;
+  infile.open("/proc/net/if_inet6");
+
+  if (!infile.good()) {
+    perror("error reading ipv6 from file");
+    exit(1);
+  }
+
+  std::string line;
+  int found = 0;
+  while (!infile.eof()) {
+    getline(infile, line);
+    // Want Ipv6 address on man interface
+    if (line.find("man") == std::string::npos) {
+      continue;
+    }
+    // Avoid local ipv6
+    if (line.substr(0, 4) == "0100" || // Discard prefix RFC 6666
+        line.substr(0, 2) == "fc" || // Unique local addresses
+        line.substr(0, 2) == "fd" ||
+        line.substr(0, 4) == "fe80" || // Link-local addresses
+        line.substr(0, 4) == "fec0") { // Old, deprecated local address range
+      continue;
+    }
+    found = 1;
+    break;
+  }
+
+  infile.close();
+  if (!found || line.size() < 32) {
+    perror("ipv6 address on man not found in file");
+    return "::1";
+  }
+
+  // Add colons
+  std::stringstream ipv6;
+  line = line.substr(0, 32);
+  for (unsigned int i = 0; i < line.size(); i++) {
+    if (i != 0 && i % 4 == 0) {
+      ipv6 << ':';
+    }
+    ipv6 << line[i];
+  }
+
+  // Format canonically
+  struct in6_addr ipv6_struct;
+  if (!inet_pton(AF_INET6, ipv6.str().c_str(), &ipv6_struct)) {
+    std::string errmsg = "unable to parse ipv6 address to inet_pton: " +
+        ipv6.str();
+    perror(errmsg.c_str());
+    exit(1);
+  }
+  char address[INET6_ADDRSTRLEN];
+  if (!inet_ntop(AF_INET6, &ipv6_struct, address, INET6_ADDRSTRLEN)) {
+    std::string errmsg = "unable to parse ipv6 address from inet_pton struct "
+        "created from: " + ipv6.str();
+    perror(errmsg.c_str());
+    exit(1);
+  }
+
+  std::string result(address);
+  return result;
+}
+
 void MakePacket(std::vector<uint8_t>* pkt) {
   devstatus::Status status;
 
@@ -101,6 +168,7 @@
   status.set_acs_contact_time(acs_contact_time);
   status.set_uptime(Uptime());
   status.set_serial(serial_number);
+  status.set_ipv6(IPAddress());
 
   pkt->resize(status.ByteSize());
   status.SerializeToArray(&(*pkt)[0], status.ByteSize());
diff --git a/cmds/test-http_bouncer.sh b/cmds/test-http_bouncer.sh
index 4129d52..9cc0d9e 100755
--- a/cmds/test-http_bouncer.sh
+++ b/cmds/test-http_bouncer.sh
@@ -40,10 +40,6 @@
 INPUTS[3]=$(printf "\n\n"; printf "$SENTINEL")
 OUTPUTS[3]=$(printf "HTTP/1.0 302 Found\r\nLocation: $URL\r\n\r\n"; printf "$SENTINEL")
 
-INPUTS[4]=$(printf "GET /GIAG2.crl HTTP/1.0\r\nHost: pki.google.com\r\n\r\n"; printf "$SENTINEL")
-OUTPUTS[4]=$(curl "http://pki.google.com/GIAG2.crl"; printf "$SENTINEL")
-STRIP_HEADER[4]=1
-
 WVSTART "http_bouncer test"
 
 # fail with no arguments
@@ -59,10 +55,13 @@
 i=0
 while [ $i -lt ${#INPUTS[@]} ]; do
   output=$(echo -n "${INPUTS[$i]}" | nc localhost $PORT; printf "$SENTINEL")
-  if [ ${STRIP_HEADER[$i]} ]; then
-    output=$(echo -n "$output" | sed '1,/^\r$/d')
-  fi
-
   WVPASSEQ "$output" "${OUTPUTS[$i]}"
   i=$(expr $i + 1)
 done
+
+# Make sure we can download a CRL even through the bouncer.
+# Some Internet Explorer versions will refuse to connect if we can't.
+WVPASS printf "GET /GIAG2.crl HTTP/1.0\r\nHost: pki.google.com\r\n\r\n" |\
+  nc localhost $PORT |\
+  sed '1,/^\r$/d' |\
+  openssl crl -inform DER
diff --git a/cmds/wifi_files.c b/cmds/wifi_files.c
index 49e77c6..1700cda 100644
--- a/cmds/wifi_files.c
+++ b/cmds/wifi_files.c
@@ -77,6 +77,11 @@
  * client for a while longer than that.
  */
 typedef struct client_state {
+  #define MAC_STR_LEN 18
+  char macstr[MAC_STR_LEN];
+  #define IFNAME_STR_LEN 16
+  char ifname[IFNAME_STR_LEN];
+
   double inactive_since;
 
   uint64_t rx_drop64;
@@ -106,14 +111,22 @@
   uint32_t tx_failed;
   uint32_t expected_mbps;
 
-  int sample_index;
 #define MAX_SAMPLE_INDEX 150
+  int rx_sample_index;
   uint8_t rx_ht_mcs_samples[MAX_SAMPLE_INDEX];
   uint8_t rx_vht_mcs_samples[MAX_SAMPLE_INDEX];
   uint8_t rx_width_samples[MAX_SAMPLE_INDEX];
   uint8_t rx_ht_nss_samples[MAX_SAMPLE_INDEX];
   uint8_t rx_vht_nss_samples[MAX_SAMPLE_INDEX];
-  uint8_t short_gi_samples[MAX_SAMPLE_INDEX];
+  uint8_t rx_short_gi_samples[MAX_SAMPLE_INDEX];
+
+  int tx_sample_index;
+  uint8_t tx_ht_mcs_samples[MAX_SAMPLE_INDEX];
+  uint8_t tx_vht_mcs_samples[MAX_SAMPLE_INDEX];
+  uint8_t tx_width_samples[MAX_SAMPLE_INDEX];
+  uint8_t tx_ht_nss_samples[MAX_SAMPLE_INDEX];
+  uint8_t tx_vht_nss_samples[MAX_SAMPLE_INDEX];
+  uint8_t tx_short_gi_samples[MAX_SAMPLE_INDEX];
 
   /*
    * Clients spend a lot of time mostly idle, where they
@@ -131,7 +144,14 @@
   uint8_t rx_width;
   uint8_t rx_ht_nss;
   uint8_t rx_vht_nss;
-  uint8_t short_gi;
+  uint8_t rx_short_gi;
+
+  uint8_t tx_ht_mcs;
+  uint8_t tx_vht_mcs;
+  uint8_t tx_width;
+  uint8_t tx_ht_nss;
+  uint8_t tx_vht_nss;
+  uint8_t tx_short_gi;
 
   /* Track the largest value we've ever seen from this client. This
    * shows client capabilities, even if current interference
@@ -141,7 +161,14 @@
   uint8_t rx_max_width;
   uint8_t rx_max_ht_nss;
   uint8_t rx_max_vht_nss;
-  uint8_t ever_short_gi;
+  uint8_t ever_rx_short_gi;
+
+  uint8_t tx_max_ht_mcs;
+  uint8_t tx_max_vht_mcs;
+  uint8_t tx_max_width;
+  uint8_t tx_max_ht_nss;
+  uint8_t tx_max_vht_nss;
+  uint8_t ever_tx_short_gi;
 
   int8_t signal;
   int8_t signal_avg;
@@ -153,11 +180,6 @@
   uint8_t mfp:1;
   uint8_t tdls_peer:1;
   uint8_t preamble_length:1;
-
-  #define MAC_STR_LEN 18
-  char macstr[MAC_STR_LEN];
-  #define IFNAME_STR_LEN 16
-  char ifname[IFNAME_STR_LEN];
 } client_state_t;
 
 
@@ -176,6 +198,16 @@
 static FILE *wifi_info_handle = NULL;
 
 
+static void ClearClientStateCounters(client_state_t *state)
+{
+  char macstr[MAC_STR_LEN];
+
+  memcpy(macstr, state->macstr, sizeof(macstr));
+  memset(state, 0, sizeof(*state));
+  memcpy(state->macstr, macstr, sizeof(state->macstr));
+}
+
+
 static int GetIfIndex(const char *ifname)
 {
   int fd;
@@ -349,7 +381,7 @@
 }
 
 
-static void GetRxMCS(struct nlattr *attr,
+static void GetMCS(struct nlattr *attr,
     int *mcs, int *vht_mcs, int *width, int *short_gi, int *vht_nss)
 {
   int w160 = 0, w80_80 = 0, w80 = 0, w40 = 0;
@@ -418,7 +450,7 @@
 }
 
 
-static int RxHtMcsToNss(int rxmcs)
+static int HtMcsToNss(int rxmcs)
 {
   /* https://en.wikipedia.org/wiki/IEEE_802.11n-2009 */
   switch(rxmcs) {
@@ -488,6 +520,12 @@
 
   mac = (uint8_t *)nla_data(tb[NL80211_ATTR_MAC]);
   state = FindClientState(mac);
+
+  if (strcasecmp(state->ifname, ifname) != 0) {
+    /* Client moved from one interface to another */
+    ClearClientStateCounters(state);
+  }
+
   state->last_seen = monotime();
   snprintf(state->ifname, sizeof(state->ifname), "%s", ifname);
 
@@ -502,20 +540,20 @@
   }
 
   if (si[NL80211_STA_INFO_RX_BITRATE]) {
-    int rx_ht_mcs=0, rx_vht_mcs=0, rx_vht_nss=0, rx_width=0, short_gi=0;
+    int rx_ht_mcs=0, rx_vht_mcs=0, rx_vht_nss=0, rx_width=0, rx_short_gi=0;
     int ht_nss;
-    int n = state->sample_index + 1;
+    int n = state->rx_sample_index + 1;
 
     if (n >= MAX_SAMPLE_INDEX) n = 0;
 
     state->rx_bitrate = GetBitrate(si[NL80211_STA_INFO_RX_BITRATE]);
-    GetRxMCS(si[NL80211_STA_INFO_RX_BITRATE], &rx_ht_mcs, &rx_vht_mcs,
-        &rx_width, &short_gi, &rx_vht_nss);
+    GetMCS(si[NL80211_STA_INFO_RX_BITRATE], &rx_ht_mcs, &rx_vht_mcs,
+        &rx_width, &rx_short_gi, &rx_vht_nss);
 
     state->rx_ht_mcs_samples[n] = rx_ht_mcs;
     if (rx_ht_mcs > state->rx_max_ht_mcs) state->rx_max_ht_mcs = rx_ht_mcs;
 
-    ht_nss = RxHtMcsToNss(rx_ht_mcs);
+    ht_nss = HtMcsToNss(rx_ht_mcs);
     state->rx_ht_nss_samples[n] = ht_nss;
     if (ht_nss > state->rx_max_ht_nss) state->rx_max_ht_nss = ht_nss;
 
@@ -525,13 +563,13 @@
     state->rx_vht_nss_samples[n] = rx_vht_nss;
     if (rx_vht_nss > state->rx_max_vht_nss) state->rx_max_vht_nss = rx_vht_nss;
 
-    state->short_gi_samples[n] = short_gi;
-    if (short_gi) state->ever_short_gi = 1;
+    state->rx_short_gi_samples[n] = rx_short_gi;
+    if (rx_short_gi) state->ever_rx_short_gi = 1;
 
     state->rx_width_samples[n] = rx_width;
     if (rx_width > state->rx_max_width) state->rx_max_width = rx_width;
 
-    state->sample_index = n;
+    state->rx_sample_index = n;
   }
   if (si[NL80211_STA_INFO_RX_BYTES]) {
     uint32_t last_rx_bytes = state->rx_bytes;
@@ -544,7 +582,36 @@
     state->rx_packets64 += (state->rx_packets - last_rx_packets);
   }
   if (si[NL80211_STA_INFO_TX_BITRATE]) {
+    int tx_ht_mcs=0, tx_vht_mcs=0, tx_vht_nss=0, tx_width=0, tx_short_gi=0;
+    int ht_nss;
+    int n = state->tx_sample_index + 1;
+
+    if (n >= MAX_SAMPLE_INDEX) n = 0;
+
     state->tx_bitrate = GetBitrate(si[NL80211_STA_INFO_TX_BITRATE]);
+    GetMCS(si[NL80211_STA_INFO_TX_BITRATE], &tx_ht_mcs, &tx_vht_mcs,
+        &tx_width, &tx_short_gi, &tx_vht_nss);
+
+    state->tx_ht_mcs_samples[n] = tx_ht_mcs;
+    if (tx_ht_mcs > state->tx_max_ht_mcs) state->tx_max_ht_mcs = tx_ht_mcs;
+
+    ht_nss = HtMcsToNss(tx_ht_mcs);
+    state->tx_ht_nss_samples[n] = ht_nss;
+    if (ht_nss > state->tx_max_ht_nss) state->tx_max_ht_nss = ht_nss;
+
+    state->tx_vht_mcs_samples[n] = tx_vht_mcs;
+    if (tx_vht_mcs > state->tx_max_vht_mcs) state->tx_max_vht_mcs = tx_vht_mcs;
+
+    state->tx_vht_nss_samples[n] = tx_vht_nss;
+    if (tx_vht_nss > state->tx_max_vht_nss) state->tx_max_vht_nss = tx_vht_nss;
+
+    state->tx_short_gi_samples[n] = tx_short_gi;
+    if (tx_short_gi) state->ever_tx_short_gi = 1;
+
+    state->tx_width_samples[n] = tx_width;
+    if (tx_width > state->tx_max_width) state->tx_max_width = tx_width;
+
+    state->tx_sample_index = n;
   }
   if (si[NL80211_STA_INFO_TX_BYTES]) {
     uint32_t last_tx_bytes = state->tx_bytes;
@@ -655,7 +722,9 @@
   client_state_t *state = (client_state_t *)value;
   int i;
   uint8_t rx_ht_mcs=0, rx_vht_mcs=0, rx_width=0, rx_ht_nss=0;
-  uint8_t rx_vht_nss=0, short_gi=0;
+  uint8_t rx_vht_nss=0, rx_short_gi=0;
+  uint8_t tx_ht_mcs=0, tx_vht_mcs=0, tx_width=0, tx_ht_nss=0;
+  uint8_t tx_vht_nss=0, tx_short_gi=0;
 
   for (i = 0; i < MAX_SAMPLE_INDEX; ++i) {
     if (state->rx_ht_mcs_samples[i] > rx_ht_mcs) {
@@ -673,8 +742,27 @@
     if (state->rx_vht_nss_samples[i] > rx_vht_nss) {
       rx_vht_nss = state->rx_vht_nss_samples[i];
     }
-    if (state->short_gi_samples[i] > short_gi) {
-      short_gi = state->short_gi_samples[i];
+    if (state->rx_short_gi_samples[i] > rx_short_gi) {
+      rx_short_gi = state->rx_short_gi_samples[i];
+    }
+
+    if (state->tx_ht_mcs_samples[i] > tx_ht_mcs) {
+      tx_ht_mcs = state->tx_ht_mcs_samples[i];
+    }
+    if (state->tx_vht_mcs_samples[i] > tx_vht_mcs) {
+      tx_vht_mcs = state->tx_vht_mcs_samples[i];
+    }
+    if (state->tx_width_samples[i] > tx_width) {
+      tx_width = state->tx_width_samples[i];
+    }
+    if (state->tx_ht_nss_samples[i] > tx_ht_nss) {
+      tx_ht_nss = state->tx_ht_nss_samples[i];
+    }
+    if (state->tx_vht_nss_samples[i] > tx_vht_nss) {
+      tx_vht_nss = state->tx_vht_nss_samples[i];
+    }
+    if (state->tx_short_gi_samples[i] > tx_short_gi) {
+      tx_short_gi = state->tx_short_gi_samples[i];
     }
   }
 
@@ -683,7 +771,14 @@
   state->rx_width = rx_width;
   state->rx_ht_nss = rx_ht_nss;
   state->rx_vht_nss = rx_vht_nss;
-  state->short_gi = short_gi;
+  state->rx_short_gi = rx_short_gi;
+
+  state->tx_ht_mcs = tx_ht_mcs;
+  state->tx_vht_mcs = tx_vht_mcs;
+  state->tx_width = tx_width;
+  state->tx_ht_nss = tx_ht_nss;
+  state->tx_vht_nss = tx_vht_nss;
+  state->tx_short_gi = tx_short_gi;
 }
 
 
@@ -738,8 +833,8 @@
   fprintf(f, "  \"rx max vht_nss\": %u,\n", state->rx_max_vht_nss);
 
   #define BOOL(x) (x ? "true" : "false")
-  fprintf(f, "  \"rx SHORT_GI\": %s,\n", BOOL(state->short_gi));
-  fprintf(f, "  \"rx SHORT_GI seen\": %s,\n", BOOL(state->ever_short_gi));
+  fprintf(f, "  \"rx SHORT_GI\": %s,\n", BOOL(state->rx_short_gi));
+  fprintf(f, "  \"rx SHORT_GI seen\": %s,\n", BOOL(state->ever_rx_short_gi));
   #undef BOOL
 
   fprintf(f, "  \"signal\": %hhd,\n", state->signal);
@@ -797,6 +892,8 @@
       "%s %s %ld %" PRIu64 ",%" PRIu64 ",%" PRIu64 ",%" PRIu64 ",%" PRIu64
       " %c,%hhd,%hhd,%u,%u,%u,%u,%u,%d"
       " %u,%u,%u,%u,%u,%d"
+      " %u,%u,%u,%u,%u,%d"
+      " %u,%u,%u,%u,%u,%d"
       "\n",
       state->macstr, state->ifname,
       ((mono_now - state->last_seen) + (state->inactive_msec / 1000)),
@@ -810,12 +907,21 @@
       state->signal, state->signal_avg,
       state->rx_ht_mcs, state->rx_ht_nss,
       state->rx_vht_mcs, state->rx_vht_nss,
-      state->rx_width, state->short_gi,
+      state->rx_width, state->rx_short_gi,
 
       /* information about the maximum we've ever seen from this client. */
       state->rx_max_ht_mcs, state->rx_max_ht_nss,
       state->rx_max_vht_mcs, state->rx_max_vht_nss,
-      state->rx_max_width, state->ever_short_gi);
+      state->rx_max_width, state->ever_rx_short_gi,
+
+      state->tx_ht_mcs, state->tx_ht_nss,
+      state->tx_vht_mcs, state->tx_vht_nss,
+      state->tx_width, state->tx_short_gi,
+
+      /* information about the maximum we've ever seen from this client. */
+      state->tx_max_ht_mcs, state->tx_max_ht_nss,
+      state->tx_max_vht_mcs, state->tx_max_vht_nss,
+      state->tx_max_width, state->ever_tx_short_gi);
 }
 
 
@@ -878,8 +984,9 @@
         if ((data[i] <= 0x1f) || !isprint(data[i])) {
           fprintf(f, "\\u00%02x", data[i]);
         } else {
-          fprintf(f, "%c", data[i]); break;
+          fprintf(f, "%c", data[i]);
         }
+        break;
     }
   }
 }
diff --git a/cmds/wifi_files_test.c b/cmds/wifi_files_test.c
index 9d48dd1..902cd73 100644
--- a/cmds/wifi_files_test.c
+++ b/cmds/wifi_files_test.c
@@ -50,6 +50,27 @@
 }
 
 
+void testPrintSsidEscapedQuoteBackslash()
+{
+  FILE *f = tmpfile();
+  char buf[32];
+  const uint8_t ssid[] = {'"', '\\'};  /* not NUL terminated. */
+  const uint8_t expected[] = {'\\', '"', '\\', '\\'};
+
+  printf("Testing \"%s\" in %s:\n", __FUNCTION__, __FILE__);
+  memset(buf, 0, sizeof(buf));
+  TEST_ASSERT(f != NULL);
+  print_ssid_escaped(f, sizeof(ssid), ssid);
+  fflush(f);
+  rewind(f);
+  TEST_ASSERT(fread(buf, 1, sizeof(buf), f) > 0);
+  printf("%s\n", buf);
+  TEST_ASSERT(memcmp(buf, expected, sizeof(expected)) == 0);
+  fclose(f);
+  printf("! %s:%d\t%s\tok\n", __FILE__, __LINE__, __FUNCTION__);
+}
+
+
 void testFrequencyToChannel()
 {
   printf("Testing \"%s\" in %s:\n", __FUNCTION__, __FILE__);
@@ -171,6 +192,7 @@
   clients = g_hash_table_new(g_str_hash, g_str_equal);
 
   testPrintSsidEscaped();
+  testPrintSsidEscapedQuoteBackslash();
   testFrequencyToChannel();
   testClientStateToJson();
   testAgeOutClients();
diff --git a/conman/connection_manager.py b/conman/connection_manager.py
index ff47820..3808458 100755
--- a/conman/connection_manager.py
+++ b/conman/connection_manager.py
@@ -213,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']
@@ -296,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)):
@@ -584,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.
@@ -690,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 e172a82..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."""
@@ -528,7 +613,8 @@
       client_mode = self._qcsapi('get_mode', 'wifi0') == 'Station'
       ssid = self._qcsapi('get_ssid', 'wifi0')
       status = self._qcsapi('get_status', 'wifi0')
-      security = self._qcsapi('ssid_get_authentication_mode', 'wifi0', ssid)
+      security = (self._qcsapi('ssid_get_authentication_mode', 'wifi0', ssid)
+                  if ssid else None)
     except subprocess.CalledProcessError:
       # If QCSAPI failed, skip update.
       return
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/hnvram/hnvram_main.c b/hnvram/hnvram_main.c
index b850048..323d62e 100644
--- a/hnvram/hnvram_main.c
+++ b/hnvram/hnvram_main.c
@@ -83,6 +83,7 @@
   {"LASER_CHANNEL",        NVRAM_FIELD_LASER_CHANNEL,     HNVRAM_STRING},
   {"MAC_ADDR_PON",         NVRAM_FIELD_MAC_ADDR_PON,      HNVRAM_MAC},
   {"PRODUCTION_UNIT",      NVRAM_FIELD_PRODUCTION_UNIT,   HNVRAM_STRING},
+  {"BOOT_TARGET",          NVRAM_FIELD_BOOT_TARGET,       HNVRAM_STRING},
 };
 
 const hnvram_field_t* get_nvram_field(const char* name) {
diff --git a/ledpattern/Makefile b/ledpattern/Makefile
index 705a398..ad3456d 100644
--- a/ledpattern/Makefile
+++ b/ledpattern/Makefile
@@ -1,7 +1,7 @@
 default:
 
-PREFIX=/
-BINDIR=$(DESTDIR)$(PREFIX)/bin
+ETCDIR=$(DESTDIR)/etc
+BINDIR=$(DESTDIR)/bin
 PYTHON?=python
 
 all:
@@ -9,6 +9,9 @@
 install:
 	mkdir -p $(BINDIR)
 	cp ledpattern.py $(BINDIR)/ledpattern
+	cp ledtapcode.sh $(BINDIR)/ledtapcode
+	cp ledpatterns $(ETCDIR)/ledpatterns
+	chmod +x $(BINDIR)/ledtapcode
 
 install-libs:
 	@echo "No libs to install."
diff --git a/ledpattern/ledpatterns b/ledpattern/ledpatterns
new file mode 100644
index 0000000..2e0ab63
--- /dev/null
+++ b/ledpattern/ledpatterns
@@ -0,0 +1,12 @@
+HALTED,P,R
+NO_LASER_CHANNEL,P,P
+SET_LASER_FAILED,P,R,R
+LOSLOF_ALARM,P,R,B
+OTHER_ALARM,P,R,P
+GPON_INITIAL,P,B,R
+GPON_STANDBY,P,B,P
+GPON_SERIAL,P,P,R
+GPON_RANGING,P,P,B
+WAIT_ACS,P,B,B
+ALL_OK,P,B,B,B
+UNKNOWN_ERROR,P,R,R,R
diff --git a/ledpattern/ledtapcode.sh b/ledpattern/ledtapcode.sh
new file mode 100755
index 0000000..6841f2f
--- /dev/null
+++ b/ledpattern/ledtapcode.sh
@@ -0,0 +1,107 @@
+#!/bin/sh
+
+. /etc/utils.sh
+
+LEDPATTERN="ledpattern /etc/ledpatterns"
+SYSFS_GPON_PATH="/sys/devices/platform/gpon"
+MONITOR_PATH="/tmp/gpio/ledcontrol"
+LASER_STATUS_FILE="/tmp/laser_i2c_status"
+ALARM_GPON_FILE="$SYSFS_GPON_PATH/info/alarmGpon"
+GPON_INFO_FILE="$SYSFS_GPON_PATH/info/infoGpon"
+HALTED_FILE="$MONITOR_PATH/halted"
+HW_FAILURE="$MONITOR_PATH/hardware_failure"
+LASER_CHANNEL_FILE="$SYSFS_GPON_PATH/misc/laserChannel"
+ACS_FILE="$MONITOR_PATH/acsconnected"
+
+PlayPatternAndExit()
+{
+  state="$1"
+  # ledpattern takes care of all the LED management and state selection.
+  result="$($LEDPATTERN $state)"
+  if [ "$?" -ne 0 ]; then
+    echo "Failed to display pattern $state: $result"
+    exit 1
+  fi
+  exit 0
+}
+
+if [ ! -f "$ALARM_GPON_FILE" ]; then
+  echo "$ALARM_GPON_FILE does not exist"
+  PlayPatternAndExit UNKNOWN_ERROR
+fi
+
+if [ ! -f "$GPON_INFO_FILE" ]; then
+  echo "$GPON_INFO_FILE does not exist"
+  PlayPatternAndExit UNKNOWN_ERROR
+fi
+
+if [ ! -f "$LASER_CHANNEL_FILE" ]; then
+  echo "$LASER_CHANNEL_FILE does not exist"
+  PlayPatternAndExit UNKNOWN_ERROR
+fi
+
+# It is a valid state that there may not be a LASER_STATUS_FILE yet.
+if [ -f "$LASER_STATUS_FILE" ]; then
+  laser_status=$(cat "$LASER_STATUS_FILE")
+  if [ "$laser_status" -ne 0 ]; then
+    echo "Playing SET_LASER_FAILED pattern"
+    PlayPatternAndExit SET_LASER_FAILED
+  fi
+fi
+
+if [ -f "$HW_FAILURE" ]; then
+  echo "Playing HALTED pattern on HW_FAILURE"
+  PlayPatternAndExit HALTED
+fi
+
+if [ -f "$HALTED_FILE" ]; then
+  echo "Playing HALTED pattern on HALTED_FILE"
+  PlayPatternAndExit HALTED
+fi
+
+# Chop the table headers off the output using tail, otherwise grep gets
+# confused later.
+alarm_info=$(cat "$ALARM_GPON_FILE" | tail -n+7)
+los_output=$(echo "$alarm_info" | grep "LOS" | grep "ON")
+lof_output=$(echo "$alarm_info" | grep "LOF" | grep "ON")
+if [ -n "$los_output" ] || [ -n "$lof_output" ]; then
+  echo "Playing LOSLOF_ALARM pattern"
+  PlayPatternAndExit LOSLOF_ALARM
+fi
+other_alarm=$(echo "$alarm_info" | grep "ON")
+if [ -n "$other_alarm" ]; then
+  echo "Playing OTHER_ALARM pattern"
+  PlayPatternAndExit OTHER_ALARM
+fi
+
+gpon_info=$(cat "$GPON_INFO_FILE" | grep "ONU STATE")
+if contains "$gpon_info" "INITIAL"; then
+  echo "Playing GPON_INITIAL pattern"
+  PlayPatternAndExit GPON_INITIAL
+elif contains "$gpon_info" "STANDBY"; then
+  echo "Playing GPON_STANDBY pattern"
+  PlayPatternAndExit GPON_STANDBY
+elif contains "$gpon_info" "SERIAL"; then
+  echo "Playing GPON_SERIAL pattern"
+  PlayPatternAndExit GPON_SERIAL
+elif contains "$gpon_info" "RANGING"; then
+  echo "Playing GPON_RANGING pattern"
+  PlayPatternAndExit GPON_RANGING
+fi
+
+laser_channel=$(cat "$LASER_CHANNEL_FILE")
+if [ ! -f "$ACS_FILE" ] && [ "$laser_channel" -eq "-1" ]; then
+  echo "Playing NO_LASER_CHANNEL pattern"
+  PlayPatternAndExit NO_LASER_CHANNEL
+elif [ ! -f "$ACS_FILE" ] && [ $laser_channel -ne "-1" ]; then
+  echo "Playing WAIT_ACS pattern"
+  PlayPatternAndExit WAIT_ACS
+elif [ -f "$ACS_FILE" ] && [ $laser_channel -ne "-1" ]; then
+  echo "Playing ALL_OK pattern"
+  PlayPatternAndExit ALL_OK
+else
+  # If we get all the way here and nothing triggered on the way then this really
+  # is an unknown error...
+  echo "Nothing triggered? Playing UNKNOWN_ERROR pattern..."
+  PlayPatternAndExit UNKNOWN_ERROR
+fi
diff --git a/wifi/iw.py b/wifi/iw.py
index ae2a8b6..c0bbf57 100644
--- a/wifi/iw.py
+++ b/wifi/iw.py
@@ -226,14 +226,26 @@
       return interface
 
 
-def find_all_interfaces_from_phy(phy):
+def find_all_interfaces_from_phy(phy, interface_type=None):
+  """Finds the names of all interfaces on a given phy.
+
+  Args:
+    phy: The name of a phy, e.g. 'phy0'.
+    interface_type: An INTERFACE_TYPE value (optional).
+
+  Returns:
+    A list of all interfaces found.
+  """
   interfaces = []
-  for interface_type in INTERFACE_TYPE:
+  interface_types = INTERFACE_TYPE
+  if interface_type:
+    interface_types = [interface_type]
+  for interface_type in interface_types:
     pattern = re.compile(r'w%s[0-9]\w*\Z' % re.escape(interface_type))
     interfaces.extend(interface for interface
                       in dev_parsed()[phy]['interfaces']
                       if pattern.match(interface))
-  return interfaces
+  return set(interfaces)
 
 
 def find_interface_from_band(band, interface_type, interface_suffix):
@@ -254,6 +266,23 @@
   return find_interface_from_phy(phy, interface_type, interface_suffix)
 
 
+def find_all_interfaces_from_band(band, interface_type=None):
+  """Finds the names of all interface on a given band.
+
+  Args:
+    band: The band for which you want the interface.
+    interface_type: An INTERFACE_TYPE value (optional).
+
+  Returns:
+    A list of all interfaces found.
+  """
+  phy = find_phy(band, 'auto')
+  if phy is None:
+    return []
+
+  return find_all_interfaces_from_phy(phy, interface_type)
+
+
 def find_width_and_channel(interface):
   """Finds the width and channel being used by a given interface.
 
diff --git a/wifi/iw_test.py b/wifi/iw_test.py
index 4a7ef4c..2293954 100755
--- a/wifi/iw_test.py
+++ b/wifi/iw_test.py
@@ -514,6 +514,19 @@
 
 
 @wvtest.wvtest
+def find_all_interfaces_from_phy_test():
+  wvtest.WVPASSEQ(set(['wlan0', 'wlan0_portal', 'wcli0']),
+                  iw.find_all_interfaces_from_phy('phy0'))
+  wvtest.WVPASSEQ(set(['wlan0', 'wlan0_portal']),
+                  iw.find_all_interfaces_from_phy('phy0', iw.INTERFACE_TYPE.ap))
+  wvtest.WVPASSEQ(set(['wcli0']),
+                  iw.find_all_interfaces_from_phy('phy0',
+                                                  iw.INTERFACE_TYPE.client))
+  wvtest.WVPASSEQ(set(['wlan1', 'wlan1_portal']),
+                  iw.find_all_interfaces_from_phy('phy1'))
+
+
+@wvtest.wvtest
 def find_interface_from_band_test():
   wvtest.WVPASSEQ('wlan0',
                   iw.find_interface_from_band('2.4', iw.INTERFACE_TYPE.ap, ''))
@@ -529,6 +542,19 @@
 
 
 @wvtest.wvtest
+def find_all_interfaces_from_band_test():
+  wvtest.WVPASSEQ(set(['wlan0', 'wlan0_portal', 'wcli0']),
+                  iw.find_all_interfaces_from_band('2.4'))
+  wvtest.WVPASSEQ(set(['wlan0', 'wlan0_portal']),
+                  iw.find_all_interfaces_from_band('2.4', iw.INTERFACE_TYPE.ap))
+  wvtest.WVPASSEQ(set(['wcli0']),
+                  iw.find_all_interfaces_from_band('2.4',
+                                                   iw.INTERFACE_TYPE.client))
+  wvtest.WVPASSEQ(set(['wlan1', 'wlan1_portal']),
+                  iw.find_all_interfaces_from_band('5'))
+
+
+@wvtest.wvtest
 def info_parsed_test():
   wvtest.WVPASSEQ({
       'wdev': '0x3',
diff --git a/wifi/wifi.py b/wifi/wifi.py
index 8797633..142010c 100755
--- a/wifi/wifi.py
+++ b/wifi/wifi.py
@@ -50,7 +50,7 @@
 X,extra-short-timeouts            Use shorter key rotations; 1=rotate PTK, 2=rotate often
 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 []
+S,interface-suffix=               Interface suffix (defaults to ALL for stop commands; use NONE to specify no suffix) []
 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
@@ -381,18 +381,25 @@
     if band == '5' and quantenna.stop_ap_wifi(opt):
       continue
 
-    interface = iw.find_interface_from_band(
-        band, iw.INTERFACE_TYPE.ap, opt.interface_suffix)
-    if interface is None:
-      utils.log('No AP interface for %s GHz; nothing to stop', band)
+    interfaces = []
+    if opt.interface_suffix == 'ALL':
+      interfaces = iw.find_all_interfaces_from_band(band, iw.INTERFACE_TYPE.ap)
+    else:
+      interface = iw.find_interface_from_band(
+          band, iw.INTERFACE_TYPE.ap, opt.interface_suffix)
+      if interface:
+        interfaces = [interface]
+    if not interfaces:
+      utils.log('No AP interfaces for %s GHz; nothing to stop', band)
       continue
 
-    if _stop_hostapd(interface):
-      if opt.persist:
-        persist.delete_options('hostapd', band)
-    else:
-      utils.log('Failed to stop hostapd on interface %s', interface)
-      success = False
+    for interface in interfaces:
+      if _stop_hostapd(interface):
+        if opt.persist:
+          persist.delete_options('hostapd', band)
+      else:
+        utils.log('Failed to stop hostapd on interface %s', interface)
+        success = False
 
   return success
 
@@ -989,18 +996,26 @@
     if band == '5' and quantenna.stop_client_wifi(opt):
       continue
 
-    interface = iw.find_interface_from_band(
-        band, iw.INTERFACE_TYPE.client, opt.interface_suffix)
-    if interface is None:
-      utils.log('No client interface for %s GHz; nothing to stop', band)
+    interfaces = []
+    if opt.interface_suffix == 'ALL':
+      interfaces = iw.find_all_interfaces_from_band(
+          band, iw.INTERFACE_TYPE.client)
+    else:
+      interface = iw.find_interface_from_band(
+          band, iw.INTERFACE_TYPE.client, opt.interface_suffix)
+      if interface:
+        interfaces = [interface]
+    if not interfaces:
+      utils.log('No client interfaces for %s GHz; nothing to stop', band)
       continue
 
-    if _stop_wpa_supplicant(interface):
-      if opt.persist:
-        persist.delete_options('wpa_supplicant', band)
-    else:
-      utils.log('Failed to stop wpa_supplicant on interface %s', interface)
-      success = False
+    for interface in interfaces:
+      if _stop_wpa_supplicant(interface):
+        if opt.persist:
+          persist.delete_options('wpa_supplicant', band)
+      else:
+        utils.log('Failed to stop wpa_supplicant on interface %s', interface)
+        success = False
 
   return success
 
@@ -1044,10 +1059,18 @@
     parser.fatal('Must specify a command (see usage for details).')
     return 1
 
+  command = extra[0]
+
   # set and setclient have a different default for -b.
-  if extra[0].startswith('set') and ' ' in opt.band:
+  if command.startswith('set') and ' ' in opt.band:
     opt.band = '2.4'
 
+  if command == 'off' or command.startswith('stop'):
+    if not opt.interface_suffix:
+      opt.interface_suffix = 'ALL'
+    elif opt.interface_suffix == 'NONE':
+      opt.interface_suffix = ''
+
   try:
     function = {
         'set': set_wifi,