Merge "ginstall: add support for platform-name loaders"
diff --git a/cmds/Makefile b/cmds/Makefile
index 14daeff..24bc8a1 100644
--- a/cmds/Makefile
+++ b/cmds/Makefile
@@ -19,6 +19,7 @@
 TARGETS=\
 	$(PORTABLE_TARGETS) \
 	alivemonitor \
+	anonid \
 	bsa2bluez \
 	burnin-flash \
 	buttonmon \
@@ -31,6 +32,7 @@
 	diskbench \
 	dnsck \
 	freemegs \
+	gfhd254_reboot \
 	gstatic \
 	http_bouncer \
 	ionice \
@@ -118,6 +120,7 @@
 	for n in $(SCRIPT_TARGETS); do \
 		test ! -f $$n.$(BR2_TARGET_GENERIC_PLATFORM_NAME) || \
 			cp -f $$n.$(BR2_TARGET_GENERIC_PLATFORM_NAME) $(BINDIR)/$$n; \
+		test ! -f $$n || cp -f $$n $(BINDIR)/$$n; \
 	done
 
 install-libs:
@@ -254,6 +257,9 @@
 		--includes --output-file=$@ $<
 hostnamelookup.tmp.o: CFLAGS += -Wno-missing-field-initializers
 host-hostnamelookup.tmp.o: CFLAGS += -Wno-missing-field-initializers
+anonid: anonid.o
+host-anonid: host-anonid.o
+anonid host-anonid: LIBS += -lcrypto
 
 
 TESTS = $(wildcard test-*.sh) $(wildcard test-*.py) $(wildcard *_test.py) $(TEST_TARGETS)
diff --git a/cmds/anonid.c b/cmds/anonid.c
new file mode 100644
index 0000000..e7854ad
--- /dev/null
+++ b/cmds/anonid.c
@@ -0,0 +1,184 @@
+/*
+ * Copyright 2015 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <fcntl.h>
+#include <getopt.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <time.h>
+#include <unistd.h>
+
+#include <openssl/md5.h>
+#include <openssl/hmac.h>
+
+
+const char SOFT[] = "AEIOUY" "V";
+const char HARD[] = "BCDFGHJKLMNPQRSTVWXYZ" "AEIOU";
+const char *consensus_key_file = "/tmp/waveguide/consensus_key";
+#define CONSENSUS_KEY_LEN 16
+uint8_t consensus_key[CONSENSUS_KEY_LEN] = {0};
+#define MAC_ADDR_LEN 17
+
+void default_consensus_key()
+{
+  int fd;
+
+  if ((fd = open("/dev/urandom", O_RDONLY)) >= 0) {
+    ssize_t siz = sizeof(consensus_key);
+    if (read(fd, consensus_key, siz) != siz) {
+      /* https://xkcd.com/221/ */
+      memset(consensus_key, time(NULL), siz);
+    }
+    close(fd);
+  }
+}
+
+/* Read the waveguide consensus_key, if any. Returns 0 if
+ * a key was present, 1 if not or something fails. */
+int get_consensus_key()
+{
+  int fd, rc = 1;
+  uint8_t new_key[sizeof(consensus_key)];
+
+  if ((fd = open(consensus_key_file, O_RDONLY)) < 0) {
+    return 1;
+  }
+
+  if (read(fd, new_key, sizeof(new_key)) == sizeof(new_key)) {
+    memcpy(consensus_key, new_key, sizeof(consensus_key));
+    rc = 0;
+  }
+  close(fd);
+
+  return rc;
+}
+
+/* Given a value from 0..4095, encode it as a cons+vowel+cons sequence. */
+void trigraph(int num, char *out)
+{
+  int ns = sizeof(SOFT) - 1;
+  int nh = sizeof(HARD) - 1;
+  int c1, c2, c3;
+
+  c3 = num % nh;
+  c2 = (num / nh) % ns;
+  c1 = num / nh / ns;
+  out[0] = HARD[c1];
+  out[1] = SOFT[c2];
+  out[2] = HARD[c3];
+}
+
+int hex_chr_to_int(char hex) {
+  switch(hex) {
+    case '0' ... '9':
+      return hex - '0';
+    case 'a' ... 'f':
+      return hex - 'a' + 10;
+    case 'A' ... 'F':
+      return hex - 'A' + 10;
+  }
+
+  return 0;
+}
+
+/*
+ * Convert a string of the form "00:11:22:33:44:55" to
+ * a binary array 001122334455.
+ */
+void get_binary_mac(const char *mac, uint8_t *out) {
+  int i;
+  for (i = 0; i < MAC_ADDR_LEN; i += 3) {
+    *out = (hex_chr_to_int(mac[i]) << 4) | hex_chr_to_int(mac[i+1]);
+    out++;
+  }
+}
+
+
+void get_anonid_for_mac(const char *mac, char *out) {
+  unsigned char digest[EVP_MAX_MD_SIZE];
+  unsigned int digest_len = sizeof(digest);
+  uint8_t macbin[6];
+  uint32_t num;
+
+  get_binary_mac(mac, macbin);
+  HMAC(EVP_md5(), consensus_key, CONSENSUS_KEY_LEN, macbin, sizeof(macbin),
+      digest, &digest_len);
+  num = (digest[0] << 16) | (digest[1] << 8) | digest[2];
+  trigraph((num >> 12) & 0x0fff, out);
+  trigraph((num      ) & 0x0fff, out + 3);
+}
+
+
+void usage(const char *progname)
+{
+  fprintf(stderr, "usage: %s: -a ##:##:##:##:##:## [-k consensus_key]\n",
+      progname);
+  fprintf(stderr, "\t-a addr: MAC address to generate an anonid for\n");
+  fprintf(stderr, "\t-k key: Use a specific consensus_key. "
+      "Default is to read it from %s\n", consensus_key_file);
+  exit(1);
+}
+
+
+int main(int argc, char **argv)
+{
+  struct option long_options[] = {
+    {"addr",          required_argument, 0, 'a'},
+    {"consensus_key", required_argument, 0, 'k'},
+    {0,          0,                 0, 0},
+  };
+  const char *addr = NULL;
+  char anonid[7];
+  size_t lim;
+  int c;
+
+  setlinebuf(stdout);
+  alarm(30);
+
+  if (get_consensus_key()) {
+    default_consensus_key();
+  }
+
+  while ((c = getopt_long(argc, argv, "a:k:", long_options, NULL)) != -1) {
+    switch (c) {
+    case 'a':
+      addr = optarg;
+      break;
+    case 'k':
+      lim = (sizeof(consensus_key) > strlen(optarg)) ? strlen(optarg) :
+        sizeof(consensus_key);
+      memset(consensus_key, 0, sizeof(consensus_key));
+      memcpy(consensus_key, optarg, lim);
+      break;
+    default:
+      usage(argv[0]);
+      break;
+    }
+  }
+
+  if (addr == NULL) {
+    usage(argv[0]);
+  }
+
+  get_anonid_for_mac(addr, anonid);
+  printf("%s\n", anonid);
+
+  exit(0);
+}
diff --git a/cmds/buttonmon.c b/cmds/buttonmon.c
index e8bab1f..31148cc 100644
--- a/cmds/buttonmon.c
+++ b/cmds/buttonmon.c
@@ -13,18 +13,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-// GFLT110 the "reset" button is connected to MPP[18]
-//
-// This will periodically scan MPP[18].
-// If held < 1s &&  sysvar PRODUCTION_UNIT is NOT set
-//     start dropbear.
-// If held > 2s
-//   generate a reset.
-// if head > 10s
-//   remove sysvar PRODUCTION_UNIT AND
-//   generate a reset.
-//
-
 
 #include <fcntl.h>
 #include <stdint.h>
@@ -38,17 +26,26 @@
 #include <time.h>
 #include <unistd.h>
 
-
-// TODO(jnewlin): Export this LED register via the gpio sysfs.
 #define GPIO_INPUT_REG_ADDR 0xf1018110
-#define RESET_BIT 18
-#define RESET_BIT_MASK (1 << RESET_BIT)
 #define TRUE 1
 #define FALSE 0
 
+/* GFLT110: The reset button is connected to MPP18 */
+#define RESET_BIT_GFLT110 18
+#define RESET_BIT_MASK_GFLT110 (1 << RESET_BIT_GFLT110)
 
-// Only run on gflt110s.
-int IsGflt110() {
+/* GFLT300: The reset button is connected to MPP17 */
+#define RESET_BIT_GFLT300 17
+#define RESET_BIT_MASK_GFLT300 (1 << RESET_BIT_GFLT300)
+
+typedef enum {
+  PlatType_GFLT110 = 1,
+  PlatType_GFLT300,
+  PlatType_Unknown
+} PlatType;
+PlatType plat_type = PlatType_Unknown;
+
+int IsSupportedPlatform() {
   int bytes_read;
   char buf[64];
   memset(buf, 0, sizeof(buf));
@@ -61,9 +58,17 @@
   fclose(f);
   if (bytes_read <= 0) {
     printf("fread of /proc/board_type returned 0 data.\n");
-  }
-  if (strncmp(buf, "GFLT110", strlen("GFLT110")))
     return FALSE;
+  }
+
+  if (strncmp(buf, "GFLT110", strlen("GFLT110")) == 0) {
+    plat_type = PlatType_GFLT110;
+  } else if (strncmp(buf, "GFLT300", strlen("GFLT300")) == 0) {
+    plat_type = PlatType_GFLT300;
+  } else {
+    /* This platform is not supported. */
+    return FALSE;
+  }
   return TRUE;
 }
 
@@ -95,12 +100,20 @@
     exit(1);
   }
 
-  volatile uint32_t* reg_addr = base + ((GPIO_INPUT_REG_ADDR & page_mask) / sizeof(*base));
+  volatile uint32_t* reg_addr = base + (
+      (GPIO_INPUT_REG_ADDR & page_mask) / sizeof(*base));
   int button_down = FALSE;
   int button_down_sent = -1;
   uint64_t button_down_start_tick = 0;
   for(;;) {
-    int button_down_now = (*reg_addr & RESET_BIT_MASK) == 0;
+    int button_down_now;
+    if (plat_type == PlatType_GFLT110)
+      button_down_now = (*reg_addr & RESET_BIT_MASK_GFLT110) == 0;
+    else if (plat_type == PlatType_GFLT300)
+      button_down_now = (*reg_addr & RESET_BIT_MASK_GFLT300) == 0;
+    else
+      button_down_now = FALSE;
+
     if (!button_down && button_down_now) {
       // Handle button down toggle.
       button_down_start_tick = GetTick();
@@ -131,8 +144,8 @@
 
 
 int main() {
-  if (!IsGflt110()) {
-    printf("resetmonitor only works on gflt110.\n");
+  if (!IsSupportedPlatform()) {
+    printf("resetmonitor only works on GFLT platforms.\n");
     return 1;
   }
   setlinebuf(stdout);
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/host-test-ssdptax.sh b/cmds/host-test-ssdptax.sh
index ebb3ae7..584401e 100755
--- a/cmds/host-test-ssdptax.sh
+++ b/cmds/host-test-ssdptax.sh
@@ -6,20 +6,34 @@
 
 SSDP=./host-ssdptax
 FIFO="/tmp/ssdptax.test.$$"
+OUTFILE="/tmp/ssdptax.test.$$.output"
 
 WVSTART "ssdptax test"
 
 python ./ssdptax-test-server.py "$FIFO" 1 &
 sleep 0.5
-WVPASSEQ "$($SSDP -t $FIFO)" "ssdp 00:00:00:00:00:00 Test Device;Google Fiber ssdptax"
-rm "$FIFO"
+WVPASS $SSDP -t "$FIFO" >"$OUTFILE"
+WVPASS grep -q "ssdp 00:00:00:00:00:00 Test Device;Google Fiber ssdptax" "$OUTFILE"
+echo quitquitquit | nc -U "$FIFO"
+rm -f "$FIFO" "$OUTFILE"
 
 python ./ssdptax-test-server.py "$FIFO" 2 &
 sleep 0.5
-WVPASSEQ "$($SSDP -t $FIFO)" "ssdp 00:00:00:00:00:00 REDACTED;server type"
-rm "$FIFO"
+WVPASS $SSDP -t "$FIFO" >"$OUTFILE"
+WVPASS grep -q "ssdp 00:00:00:00:00:00 REDACTED;server type" "$OUTFILE"
+echo quitquitquit | nc -U "$FIFO"
+rm -f "$FIFO" "$OUTFILE"
 
 python ./ssdptax-test-server.py "$FIFO" 3 &
 sleep 0.5
-WVPASSEQ "$($SSDP -t $FIFO)" "ssdp 00:00:00:00:00:00 Unknown;server type"
-rm "$FIFO"
+WVPASS $SSDP -t "$FIFO" >"$OUTFILE"
+WVPASS grep -q "ssdp 00:00:00:00:00:00 Unknown;server type" "$OUTFILE"
+echo quitquitquit | nc -U "$FIFO"
+rm -f "$FIFO" "$OUTFILE"
+
+python ./ssdptax-test-server.py "$FIFO" 4 &
+sleep 0.5
+WVPASS $SSDP -t "$FIFO" >"$OUTFILE"
+WVPASS grep -q "ssdp 00:00:00:00:00:00 Test Device;Google Fiber ssdptax multicast" "$OUTFILE"
+echo quitquitquit | nc -U "$FIFO"
+rm -f "$FIFO" "$OUTFILE"
diff --git a/cmds/logos.c b/cmds/logos.c
index 93577ce..57a6d02 100644
--- a/cmds/logos.c
+++ b/cmds/logos.c
@@ -25,7 +25,6 @@
  *  - cleans up control characters (ie. chars < 32).
  *  - makes sure output lines are in "facility: message" format.
  *  - doesn't rely on syslogd.
- *  - suppresses logging of filenames of personal media.
  */
 #include <assert.h>
 #include <ctype.h>
@@ -460,48 +459,6 @@
 }
 
 
-/*
- * Return true for a character which we expect to terminate a
- * media filename.
- */
-static int is_filename_terminator(char c) {
-  switch(c) {
-    case ' ':
-    case '\'':
-    case '"':
-      return 1;
-  }
-
-  return 0;
-}
-
-/*
- * search for text patterns which look like filenames of
- * personal media, and cross out the filename portion with
- * 'X' characters.
- */
-static void suppress_media_filenames(uint8_t *line, ssize_t len,
-                                     const char *path) {
-  uint8_t *s = line;
-  ssize_t pathlen = strlen(path);
-
-  while (len > pathlen) {
-    if (strncmp((char *)s, path, pathlen) == 0) {
-      /* Found a filename, blot it out. */
-      s += pathlen;
-      len -= pathlen;
-      while (len > 0 && !is_filename_terminator(*s)) {
-        *s++ = 'X';
-        len--;
-      }
-    } else {
-      s += 1;
-      len -= 1;
-    }
-  }
-}
-
-
 static void usage(void) {
   fprintf(stderr,
       "Usage: [LOGOS_DEBUG=1] logos <facilityname> [bytes/burst] [bytes/day]\n"
@@ -657,8 +614,6 @@
       uint8_t *start = buf, *next = buf + used, *end = buf + used + got, *p;
       while ((p = memchr(next, '\n', end - next)) != NULL) {
         ssize_t linelen = p - start;
-        suppress_media_filenames(start, linelen, "/var/media/pictures/");
-        suppress_media_filenames(start, linelen, "/var/media/videos/");
         flush(header, headerlen, start, linelen);
         if (overlong) {
           // that flush() was the first newline after buffer length
diff --git a/cmds/ssdptax-test-server.py b/cmds/ssdptax-test-server.py
index 54831d4..c86283a 100644
--- a/cmds/ssdptax-test-server.py
+++ b/cmds/ssdptax-test-server.py
@@ -5,7 +5,9 @@
 
 import BaseHTTPServer
 import socket
+import SocketServer
 import sys
+import threading
 
 
 text_device_xml = """<root>
@@ -31,45 +33,107 @@
   <device></device></root>"""
 
 
-xml = ['']
+ssdp_device_xml = """<root>
+  <specVersion><major>1</major><minor>0</minor></specVersion>
+  <device><friendlyName>Test Device</friendlyName>
+  <manufacturer>Google Fiber</manufacturer>
+  <modelDescription>Unit Test</modelDescription>
+  <modelName>ssdptax multicast</modelName>
+</device></root>"""
 
 
-class XmlHandler(BaseHTTPServer.BaseHTTPRequestHandler):
+notify_template = 'NOTIFY\r\nHOST:239.255.255.250:1900\r\nLOCATION:%s\r\n'
+notify_text = ['']
+
+
+minissdpd_response = ['']
+keep_running = [True]
+
+
+class HttpHandler(BaseHTTPServer.BaseHTTPRequestHandler):
+  """Respond to an HHTP GET for SSDP DeviceInfo."""
+
   def do_GET(self):
     self.send_response(200)
     self.send_header('Content-type','text/xml')
     self.end_headers()
-    self.wfile.write(xml[0])
+    if self.path.endswith('text_device_xml'):
+      self.wfile.write(text_device_xml)
+    if self.path.endswith('email_address_xml'):
+      self.wfile.write(email_address_xml)
+    if self.path.endswith('no_friendlyname_xml'):
+      self.wfile.write(no_friendlyname_xml)
+    if self.path.endswith('ssdp_device_xml'):
+      self.wfile.write(ssdp_device_xml)
+
+
+class ThreadingHTTPServer(SocketServer.ThreadingMixIn, BaseHTTPServer.HTTPServer):
+  pass
+
+
+class UnixHandler(SocketServer.StreamRequestHandler):
+  """Respond to a command on MiniSSDPd's Unix socket."""
+
+  def handle(self):
+    data = self.request.recv(8192)
+    if 'quitquitquit' in data:
+      print 'Received quitquitquit, exiting...'
+      keep_running[0] = False
+      return
+    else:
+      self.request.sendall(bytearray(minissdpd_response[0]))
+
+
+class UdpHandler(SocketServer.DatagramRequestHandler):
+  def handle(self):
+    self.request[1].sendto(bytearray(notify_text[0]), self.client_address)
+
+
+class ThreadingUdpServer(SocketServer.ThreadingUDPServer):
+  allow_reuse_address = True
 
 
 def main():
-  un = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
-  un.bind(sys.argv[1])
-  un.listen(1)
-  conn, _ = un.accept()
-
+  socketpath = sys.argv[1]
   testnum = int(sys.argv[2])
   if testnum == 1:
-    xml[0] = text_device_xml
+    pathend = 'text_device_xml'
   if testnum == 2:
-    xml[0] = email_address_xml
+    pathend = 'email_address_xml'
   if testnum == 3:
-    xml[0] = no_friendlyname_xml
+    pathend = 'no_friendlyname_xml'
+  if testnum == 4:
+    pathend = 'ssdp_device_xml'
 
-  s = BaseHTTPServer.HTTPServer(("", 0), XmlHandler)
-  sn = s.socket.getsockname()
+  h = ThreadingHTTPServer(("", 0), HttpHandler)
+  sn = h.socket.getsockname()
   port = sn[1]
-  url = 'http://127.0.0.1:%d/foo.xml' % port
+  url = 'http://127.0.0.1:%d/%s' % (port, pathend)
   st = 'server type'
   uuid = 'uuid goes here'
-  data = [1]
-  data.extend([len(url)] + list(url))
-  data.extend([len(st)] + list(st))
-  data.extend([len(uuid)] + list(uuid))
+  if testnum == 4:
+    minissdpd_response[0] = [0]
+  else:
+    minissdpd_response[0] = [1]
+    minissdpd_response[0].extend([len(url)] + list(url))
+    minissdpd_response[0].extend([len(st)] + list(st))
+    minissdpd_response[0].extend([len(uuid)] + list(uuid))
+  notify_text[0] = notify_template % url
 
-  _ = conn.recv(8192)
-  conn.sendall(bytearray(data))
-  s.handle_request()
+  h_thread = threading.Thread(target=h.serve_forever)
+  h_thread.daemon = True
+  h_thread.start()
+
+  d = ThreadingUdpServer(('', 1900), UdpHandler)
+  d.socket.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP,
+      socket.inet_aton('239.255.255.250') + socket.inet_aton('0.0.0.0'))
+  d_thread = threading.Thread(target=d.serve_forever)
+  d_thread.daemon = True
+  d_thread.start()
+
+  u = SocketServer.UnixStreamServer(socketpath, UnixHandler)
+  while keep_running[0]:
+    u.handle_request()
 
 
 if __name__ == '__main__':
diff --git a/cmds/ssdptax.cc b/cmds/ssdptax.cc
index 2a06c7a..d2663fc 100644
--- a/cmds/ssdptax.cc
+++ b/cmds/ssdptax.cc
@@ -30,6 +30,7 @@
 #include <ctype.h>
 #include <curl/curl.h>
 #include <getopt.h>
+#include <net/if.h>
 #include <netinet/in.h>
 #include <regex.h>
 #include <stdio.h>
@@ -43,6 +44,7 @@
 
 #include <iostream>
 #include <set>
+#include <tr1/unordered_map>
 
 #include "l2utils.h"
 
@@ -68,10 +70,11 @@
 
 typedef struct ssdp_info {
   ssdp_info(): srv_type(), url(), friendlyName(), ipaddr(),
-    manufacturer(), model(), failed(0) {}
+    manufacturer(), model(), buffer(), failed(0) {}
   ssdp_info(const ssdp_info& s): srv_type(s.srv_type), url(s.url),
     friendlyName(s.friendlyName), ipaddr(s.ipaddr),
-    manufacturer(s.manufacturer), model(s.model), failed(s.failed) {}
+    manufacturer(s.manufacturer), model(s.model),
+    buffer(s.buffer), failed(s.failed) {}
   std::string srv_type;
   std::string url;
   std::string friendlyName;
@@ -84,6 +87,24 @@
 } ssdp_info_t;
 
 
+typedef std::tr1::unordered_map<std::string, ssdp_info_t*> ResponsesMap;
+
+
+int ssdp_loop = 0;
+
+
+/* SSDP Discover packet */
+#define SSDP_PORT 1900
+#define SSDP_IP4  "239.255.255.250"
+#define SSDP_IP6  "ff02::c"
+const char discover_template[] = "M-SEARCH * HTTP/1.1\r\n"
+                                 "HOST: %s:%d\r\n"
+                                 "MAN: \"ssdp:discover\"\r\n"
+                                 "MX: 2\r\n"
+                                 "USER-AGENT: ssdptax/1.0\r\n"
+                                 "ST: %s\r\n\r\n";
+
+
 static void strncpy_limited(char *dst, size_t dstlen,
     const char *src, size_t srclen)
 {
@@ -104,6 +125,13 @@
 }
 
 
+static time_t monotime(void) {
+  struct timespec ts;
+  clock_gettime(CLOCK_MONOTONIC, &ts);
+  return ts.tv_sec;
+}
+
+
 /*
  * Send a request to minissdpd. Returns a std::string containing
  * minissdpd's response.
@@ -124,19 +152,19 @@
 
   if (s < 0) {
     perror("socket AF_UNIX failed");
-    exit(1);
+    return rc;
   }
   memset(&addr, 0, sizeof(addr));
   addr.sun_family = AF_UNIX;
   strncpy(addr.sun_path, sock_path, sizeof(addr.sun_path));
-  if(connect(s, (struct sockaddr *)&addr, sizeof(struct sockaddr_un)) < 0) {
+  if (connect(s, (struct sockaddr *)&addr, sizeof(struct sockaddr_un)) < 0) {
     perror("connect to minisspd failed");
-    exit(1);
+    return rc;
   }
 
   if ((buffer = (char *)malloc(siz)) == NULL) {
     fprintf(stderr, "malloc(%zu) failed\n", siz);
-    exit(1);
+    return rc;
   }
   memset(buffer, 0, siz);
 
@@ -147,7 +175,8 @@
   p += device_len;
   if (write(s, buffer, p - buffer) < 0) {
     perror("write to minissdpd failed");
-    exit(1);
+    free(buffer);
+    return rc;
   }
 
   FD_ZERO(&readfds);
@@ -157,18 +186,174 @@
 
   if (select(s + 1, &readfds, NULL, NULL, &tv) < 1) {
     fprintf(stderr, "select failed\n");
-    exit(1);
+    free(buffer);
+    return rc;
   }
 
   if ((len = read(s, buffer, siz)) < 0) {
     perror("read from minissdpd failed");
-    exit(1);
+    free(buffer);
+    return rc;
   }
 
   close(s);
   rc = std::string(buffer, len);
   free(buffer);
-  return(rc);
+  return rc;
+}
+
+
+int get_ipv4_ssdp_socket()
+{
+  int s;
+  int reuse = 1;
+  struct sockaddr_in sin;
+  struct ip_mreq mreq;
+  struct ip_mreqn mreqn;
+
+  if ((s = socket(AF_INET, SOCK_DGRAM, 0)) < 0) {
+    perror("socket SOCK_DGRAM");
+    exit(1);
+  }
+
+  if (setsockopt(s, SOL_SOCKET, SO_REUSEADDR, (char *)&reuse, sizeof(reuse))) {
+    perror("setsockopt SO_REUSEADDR");
+    exit(1);
+  }
+
+  if (setsockopt(s, IPPROTO_IP, IP_MULTICAST_LOOP,
+        &ssdp_loop, sizeof(ssdp_loop))) {
+    perror("setsockopt IP_MULTICAST_LOOP");
+    exit(1);
+  }
+
+  memset(&sin, 0, sizeof(sin));
+  sin.sin_family = AF_INET;
+  sin.sin_port = htons(SSDP_PORT);
+  sin.sin_addr.s_addr = INADDR_ANY;
+  if (bind(s, (struct sockaddr*)&sin, sizeof(sin))) {
+    perror("bind");
+    exit(1);
+  }
+
+  memset(&mreqn, 0, sizeof(mreqn));
+  mreqn.imr_ifindex = if_nametoindex("br0");
+  if (setsockopt(s, IPPROTO_IP, IP_MULTICAST_IF, &mreqn, sizeof(mreqn))) {
+    perror("IP_MULTICAST_IF");
+    exit(1);
+  }
+
+  memset(&mreq, 0, sizeof(mreq));
+  mreq.imr_multiaddr.s_addr = inet_addr(SSDP_IP4);
+  if (setsockopt(s, IPPROTO_IP, IP_ADD_MEMBERSHIP,
+        (char *)&mreq, sizeof(mreq))) {
+    perror("IP_ADD_MEMBERSHIP");
+    exit(1);
+  }
+
+  return s;
+}
+
+
+void send_ssdp_ip4_request(int s, const char *search)
+{
+  struct sockaddr_in sin;
+  char buf[1024];
+  ssize_t len;
+
+  snprintf(buf, sizeof(buf), discover_template, SSDP_IP4, SSDP_PORT, search);
+  memset(&sin, 0, sizeof(sin));
+  sin.sin_family = AF_INET;
+  sin.sin_port = htons(SSDP_PORT);
+  sin.sin_addr.s_addr = inet_addr(SSDP_IP4);
+  len = strlen(buf);
+  if (sendto(s, buf, len, 0, (struct sockaddr*)&sin, sizeof(sin)) != len) {
+    perror("sendto multicast IPv4");
+    exit(1);
+  }
+}
+
+
+int get_ipv6_ssdp_socket()
+{
+  int s;
+  int reuse = 1;
+  struct sockaddr_in6 sin6;
+  struct ipv6_mreq mreq;
+  int idx;
+  int hops;
+
+  if ((s = socket(AF_INET6, SOCK_DGRAM, 0)) < 0) {
+    perror("socket SOCK_DGRAM");
+    exit(1);
+  }
+
+  if (setsockopt(s, SOL_SOCKET, SO_REUSEADDR, (char *)&reuse, sizeof(reuse))) {
+    perror("setsockopt SO_REUSEADDR");
+    exit(1);
+  }
+
+  if (setsockopt(s, IPPROTO_IPV6, IPV6_MULTICAST_LOOP,
+        &ssdp_loop, sizeof(ssdp_loop))) {
+    perror("setsockopt IPV6_MULTICAST_LOOP");
+    exit(1);
+  }
+
+  memset(&sin6, 0, sizeof(sin6));
+  sin6.sin6_family = AF_INET6;
+  sin6.sin6_port = htons(SSDP_PORT);
+  sin6.sin6_addr = in6addr_any;
+  if (bind(s, (struct sockaddr*)&sin6, sizeof(sin6))) {
+    perror("bind");
+    exit(1);
+  }
+
+  idx = if_nametoindex("br0");
+  if (setsockopt(s, IPPROTO_IPV6, IPV6_MULTICAST_IF, &idx, sizeof(idx))) {
+    perror("IP_MULTICAST_IF");
+    exit(1);
+  }
+
+  hops = 2;
+  if (setsockopt(s, IPPROTO_IPV6, IPV6_MULTICAST_HOPS, &hops, sizeof(hops))) {
+    perror("IPV6_MULTICAST_HOPS");
+    exit(1);
+  }
+
+  memset(&mreq, 0, sizeof(mreq));
+  mreq.ipv6mr_interface = idx;
+  if (inet_pton(AF_INET6, SSDP_IP6, &mreq.ipv6mr_multiaddr) != 1) {
+    fprintf(stderr, "ERR: inet_pton(%s) failed", SSDP_IP6);
+    exit(1);
+  }
+  if (setsockopt(s, IPPROTO_IPV6, IPV6_JOIN_GROUP, &mreq, sizeof(mreq)) < 0) {
+    perror("ERR: setsockopt(IPV6_JOIN_GROUP)");
+    exit(1);
+  }
+
+  return s;
+}
+
+
+void send_ssdp_ip6_request(int s, const char *search)
+{
+  struct sockaddr_in6 sin6;
+  char buf[1024];
+  ssize_t len;
+
+  snprintf(buf, sizeof(buf), discover_template, SSDP_IP6, SSDP_PORT, search);
+  memset(&sin6, 0, sizeof(sin6));
+  sin6.sin6_family = AF_INET6;
+  sin6.sin6_port = htons(SSDP_PORT);
+  if (inet_pton(AF_INET6, SSDP_IP6, &sin6.sin6_addr) != 1) {
+    fprintf(stderr, "ERR: inet_pton(%s) failed", SSDP_IP6);
+    exit(1);
+  }
+  len = strlen(buf);
+  if (sendto(s, buf, len, 0, (struct sockaddr*)&sin6, sizeof(sin6)) != len) {
+    perror("sendto multicast IPv6");
+    exit(1);
+  }
 }
 
 
@@ -389,8 +574,102 @@
 }
 
 
+std::string trim(std::string s)
+{
+  size_t start = s.find_first_not_of(" \t\v\f\b\r\n");
+  if (std::string::npos != start && 0 != start) s = s.erase(0, start);
+
+  size_t end = s.find_last_not_of(" \t\v\f\b\r\n");
+  if (std::string::npos != end) s = s.substr(0, end + 1);
+
+  return s;
+}
+
+
+void parse_ssdp_response(int s, ResponsesMap &responses)
+{
+  ssdp_info_t *info = new ssdp_info_t;
+  char buffer[4096];
+  char *p, *saveptr, *strtok_pos;
+  ssize_t pktlen;
+
+  memset(buffer, 0, sizeof(buffer));
+  pktlen = recv(s, buffer, sizeof(buffer) - 1, 0);
+  if (pktlen < 0 || (size_t)pktlen >= sizeof(buffer)) {
+    fprintf(stderr, "error receiving SSDP response, pktlen=%zd\n", pktlen);
+    delete info;
+    /* not fatal, just return */
+    return;
+  }
+  buffer[pktlen] = '\0';
+  strtok_pos = buffer;
+
+  while ((p = strtok_r(strtok_pos, "\r\n", &saveptr)) != NULL) {
+    if (strlen(p) > 9 && strncasecmp(p, "location:", 9) == 0) {
+      char urlbuf[512];
+      p += 9;
+      strncpy_limited(urlbuf, sizeof(urlbuf), p, strlen(p));
+      info->url = trim(std::string(urlbuf, strlen(urlbuf)));
+    } else if (strlen(p) > 7 && strncasecmp(p, "server:", 7) == 0) {
+      char srv_type_buf[256];
+      p += 7;
+      strncpy_limited(srv_type_buf, sizeof(srv_type_buf), p, strlen(p));
+      info->srv_type = trim(std::string(srv_type_buf, strlen(srv_type_buf)));
+    }
+    strtok_pos = NULL;
+  }
+
+  if (info->url.length() && responses.find(info->url) == responses.end()) {
+    fetch_device_info(info->url, info);
+    responses[info->url] = info;
+  } else {
+    delete info;
+  }
+}
+
+
+/* Wait for SSDP NOTIFY messages to arrive. */
+#define TIMEOUT_SECS  5
+void listen_for_responses(int s4, int s6, ResponsesMap &responses)
+{
+  struct timeval tv;
+  fd_set rfds;
+  int maxfd = (s4 > s6) ? s4 : s6;
+  time_t start = monotime();
+
+  memset(&tv, 0, sizeof(tv));
+  tv.tv_sec = TIMEOUT_SECS;
+  tv.tv_usec = 0;
+
+  FD_ZERO(&rfds);
+  FD_SET(s4, &rfds);
+  FD_SET(s6, &rfds);
+
+  while (select(maxfd + 1, &rfds, NULL, NULL, &tv) > 0) {
+    time_t end = monotime();
+    if (FD_ISSET(s4, &rfds)) {
+      parse_ssdp_response(s4, responses);
+    }
+    if (FD_ISSET(s6, &rfds)) {
+      parse_ssdp_response(s6, responses);
+    }
+
+    FD_ZERO(&rfds);
+    FD_SET(s4, &rfds);
+    FD_SET(s6, &rfds);
+
+    if ((end - start) > TIMEOUT_SECS) {
+      /* even on a network filled with SSDP packets,
+       * return after TIMEOUT_SECS. */
+      break;
+    }
+  }
+}
+
+
 void usage(char *progname) {
-  printf("usage: %s [-t /path/to/fifo]\n", progname);
+  printf("usage: %s [-t /path/to/fifo] [-s search]\n", progname);
+  printf("\t-s\tserver type to search for (default ssdp:all)\n");
   printf("\t-t\ttest mode, use a fake path instead of minissdpd.\n");
   exit(1);
 }
@@ -399,11 +678,11 @@
 int main(int argc, char **argv)
 {
   std::string buffer;
-  typedef std::tr1::unordered_map<std::string, ssdp_info_t*> ResponsesMap;
   ResponsesMap responses;
   L2Map l2map;
-  int c, num;
+  int c, s4, s6;
   const char *sock_path = SOCK_PATH;
+  const char *search = "ssdp:all";
 
   setlinebuf(stdout);
   alarm(30);
@@ -413,28 +692,52 @@
     exit(1);
   }
 
-  while ((c = getopt(argc, argv, "t:")) != -1) {
+  while ((c = getopt(argc, argv, "s:t:")) != -1) {
     switch(c) {
-      case 't': sock_path = optarg; break;
+      case 's': search = optarg; break;
+      case 't':
+        sock_path = optarg;
+        ssdp_loop = 1;
+        break;
       default: usage(argv[0]); break;
     }
   }
 
-  buffer = request_from_ssdpd(sock_path, 3, "ssdp:all");
-  num = buffer.c_str()[0];
-  buffer.erase(0, 1);
-  while ((num-- > 0) && buffer.length() > 0) {
-    ssdp_info_t *info = new ssdp_info_t;
+  /* Request the list from MiniSSDPd */
+  buffer = request_from_ssdpd(sock_path, 3, search);
+  if (!buffer.empty()) {
+    int num = buffer.c_str()[0];
+    buffer.erase(0, 1);
+    while ((num-- > 0) && buffer.length() > 0) {
+      ssdp_info_t *info = new ssdp_info_t;
 
-    parse_minissdpd_response(buffer, info->url, info->srv_type);
-    if (info->url.length() && responses.find(info->url) == responses.end()) {
-      fetch_device_info(info->url, info);
-      responses[info->url] = info;
-    } else {
-      delete info;
+      parse_minissdpd_response(buffer, info->url, info->srv_type);
+      if (info->url.length() && responses.find(info->url) == responses.end()) {
+        fetch_device_info(info->url, info);
+        responses[info->url] = info;
+      } else {
+        delete info;
+      }
     }
+
+    /* Capture the ARP table in its current state. */
+    get_l2_map(&l2map);
   }
 
+  /* Supplement what we got from MiniSSDPd by sending
+   * our own M-SEARCH and listening for responses. */
+  s4 = get_ipv4_ssdp_socket();
+  send_ssdp_ip4_request(s4, search);
+  s6 = get_ipv6_ssdp_socket();
+  send_ssdp_ip6_request(s6, search);
+  listen_for_responses(s4, s6, responses);
+  close(s4);
+  s4 = -1;
+  close(s6);
+  s6 = -1;
+
+  /* Capture any new ARP table entries which appeared after sending
+   * our own M-SEARCH. */
   get_l2_map(&l2map);
 
   typedef std::set<std::string> ResultsSet;
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-anonid.sh b/cmds/test-anonid.sh
new file mode 100755
index 0000000..87f2b0f
--- /dev/null
+++ b/cmds/test-anonid.sh
@@ -0,0 +1,8 @@
+#!/bin/bash
+. ./wvtest/wvtest.sh
+
+WVSTART "anonid test"
+ANONID="./host-anonid"
+
+WVPASSEQ "$($ANONID -a 00:11:22:33:44:55 -k 0123456789)" "KEALAE"
+WVPASSEQ "$($ANONID -a 00:11:22:33:44:66 -k 6789abcdef)" "AAKLYK"
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/test-logos.py b/cmds/test-logos.py
index d930ccb..34f5693 100755
--- a/cmds/test-logos.py
+++ b/cmds/test-logos.py
@@ -86,23 +86,6 @@
   os.write(fd1, '\n')
   WVPASSEQ('<7>fac: booga!\n', _Read())
 
-  # Filenames
-  os.write(fd1, 'Accessing /var/media/pictures/MyPicture.jpg for decode\n')
-  WVPASSEQ('<7>fac: Accessing /var/media/pictures/XXXXXXXXXXXXX for decode\n',
-           _Read())
-  os.write(fd1, '/var/media/pictures/MyPicture.jpg\n')
-  WVPASSEQ('<7>fac: /var/media/pictures/XXXXXXXXXXXXX\n',
-           _Read())
-  os.write(fd1, 'Accessing /var/media/videos/MyMovie.mpg for decode\n')
-  WVPASSEQ('<7>fac: Accessing /var/media/videos/XXXXXXXXXXX for decode\n',
-           _Read())
-  os.write(fd1, 'Accessing /var/media/tv/MyTvShow.ts for decode\n')
-  WVPASSEQ('<7>fac: Accessing /var/media/tv/MyTvShow.ts for decode\n',
-           _Read())
-  os.write(fd1, 'check "/var/media/videos/MyTvShow.ts"len=1024\n')
-  WVPASSEQ('<7>fac: check "/var/media/videos/XXXXXXXXXXX"len=1024\n',
-           _Read())
-
   # rate limiting
   os.write(fd1, (('x'*80) + '\n') * 500)
   result = ''
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 6ccdaa2..3808458 100755
--- a/conman/connection_manager.py
+++ b/conman/connection_manager.py
@@ -10,6 +10,7 @@
 import os
 import random
 import re
+import socket
 import subprocess
 import time
 
@@ -19,11 +20,18 @@
 import pyinotify
 
 import cycler
+import experiment
 import interface
 import iw
 import status
 
 
+HOSTNAME = socket.gethostname()
+TMP_HOSTS = '/tmp/hosts'
+
+experiment.register('WifiNo2GClient')
+
+
 class FileChangeHandler(pyinotify.ProcessEvent):
   """Connects pyinotify events to ConnectionManager."""
 
@@ -89,13 +97,15 @@
       raise ValueError('Command file does not specify SSID')
 
     if self.wifi.initial_ssid == self.ssid:
-      logging.debug('Connected to WLAN at startup')
+      logging.info('Connected to WLAN at startup')
 
   @property
   def client_up(self):
     wpa_status = self.wifi.wpa_status()
     return (wpa_status.get('wpa_state') == 'COMPLETED'
-            and wpa_status.get('ssid') == self.ssid)
+            # NONE indicates we're on a provisioning network; anything else
+            # suggests we're already on the WLAN.
+            and wpa_status.get('key_mgmt') != 'NONE')
 
   def start_access_point(self):
     """Start an access point."""
@@ -111,7 +121,7 @@
     try:
       subprocess.check_output(self.command, stderr=subprocess.STDOUT)
       self.access_point_up = True
-      logging.debug('Started %s GHz AP', self.band)
+      logging.info('Started %s GHz AP', self.band)
     except subprocess.CalledProcessError as e:
       logging.error('Failed to start access point: %s', e.output)
 
@@ -126,18 +136,31 @@
     try:
       subprocess.check_output(command, stderr=subprocess.STDOUT)
       self.access_point_up = False
-      logging.debug('Stopped %s GHz AP', self.band)
+      logging.info('Stopped %s GHz AP', self.band)
     except subprocess.CalledProcessError as e:
       logging.error('Failed to stop access point: %s', e.output)
       return
 
   def start_client(self):
     """Join the WLAN as a client."""
+    if experiment.enabled('WifiNo2GClient') and self.band == '2.4':
+      logging.info('WifiNo2GClient enabled; not starting 2.4 GHz client.')
+      return
+
     up = self.client_up
     if up:
       logging.debug('Wifi client already started on %s GHz', self.band)
       return
 
+    if self._actually_start_client():
+      self._post_start_client()
+
+  def _actually_start_client(self):
+    """Actually run wifi setclient.
+
+    Returns:
+      Whether the command succeeded.
+    """
     command = self.WIFI_SETCLIENT + ['--ssid', self.ssid, '--band', self.band]
     env = dict(os.environ)
     if self.passphrase:
@@ -147,8 +170,12 @@
       subprocess.check_output(command, stderr=subprocess.STDOUT, env=env)
     except subprocess.CalledProcessError as e:
       logging.error('Failed to start wifi client: %s', e.output)
-      return
+      self._status.wlan_failed = True
+      return False
 
+    return True
+
+  def _post_start_client(self):
     self._status.connected_to_wlan = True
     logging.info('Started wifi client on %s GHz', self.band)
     self.wifi.attach_wpa_control(self._wpa_control_interface)
@@ -165,7 +192,7 @@
                               stderr=subprocess.STDOUT)
       # TODO(rofrankel): Make this work for dual-radio devices.
       self._status.connected_to_wlan = False
-      logging.debug('Stopped wifi client on %s GHz', self.band)
+      logging.info('Stopped wifi client on %s GHz', self.band)
     except subprocess.CalledProcessError as e:
       logging.error('Failed to stop wifi client: %s', e.output)
 
@@ -186,12 +213,14 @@
   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']
   IP_LINK = ['ip', 'link']
   IFPLUGD_ACTION = ['/etc/ifplugd/ifplugd.action']
   BINWIFI = ['wifi']
+  UPLOAD_LOGS_AND_WAIT = ['timeout', '60', 'upload-logs-and-wait']
 
   def __init__(self,
                bridge_interface='br0',
@@ -201,7 +230,7 @@
                wpa_control_interface='/var/run/wpa_supplicant',
                run_duration_s=1, interface_update_period=5,
                wifi_scan_period_s=120, wlan_retry_s=15, acs_update_wait_s=10,
-               bssid_cycle_length_s=30):
+               dhcp_wait_s=10, bssid_cycle_length_s=30):
 
     self._tmp_dir = tmp_dir
     self._config_dir = config_dir
@@ -214,8 +243,10 @@
     self._wifi_scan_period_s = wifi_scan_period_s
     self._wlan_retry_s = wlan_retry_s
     self._acs_update_wait_s = acs_update_wait_s
+    self._dhcp_wait_s = dhcp_wait_s
     self._bssid_cycle_length_s = bssid_cycle_length_s
     self._wlan_configuration = {}
+    self._try_to_upload_logs = False
 
     # Make sure all necessary directories exist.
     for directory in (self._tmp_dir, self._config_dir, self._moca_tmp_dir,
@@ -266,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)):
@@ -294,7 +326,7 @@
     # the routing table.
     for ifc in [self.bridge] + self.wifi:
       ifc.initialize()
-      logging.debug('%s initialized', ifc.name)
+      logging.info('%s initialized', ifc.name)
 
     self._interface_update_counter = 0
     self._try_wlan_after = {'5': 0, '2.4': 0}
@@ -425,7 +457,7 @@
       if self._connected_to_wlan(wifi):
         self._status.connected_to_wlan = True
         logging.debug('Connected to WLAN on %s, nothing else to do.', wifi.name)
-        return
+        break
 
       # This interface is not connected to the WLAN, so scan for potential
       # routes to the ACS for provisioning.
@@ -443,10 +475,10 @@
       for band in wifi.bands:
         wlan_configuration = self._wlan_configuration.get(band, None)
         if wlan_configuration and time.time() > self._try_wlan_after[band]:
-          logging.debug('Trying to join WLAN on %s.', wifi.name)
+          logging.info('Trying to join WLAN on %s.', wifi.name)
           wlan_configuration.start_client()
           if self._connected_to_wlan(wifi):
-            logging.debug('Joined WLAN on %s.', wifi.name)
+            logging.info('Joined WLAN on %s.', wifi.name)
             self._status.connected_to_wlan = True
             self._try_wlan_after[band] = 0
             break
@@ -464,10 +496,15 @@
         logging.debug('Unable to join WLAN on %s', wifi.name)
         self._status.connected_to_wlan = False
         if self.acs():
-          logging.debug('Connected to ACS on %s', wifi.name)
-          wifi.last_successful_bss_info = getattr(wifi,
-                                                  'last_attempted_bss_info',
-                                                  None)
+          logging.debug('Connected to ACS')
+          if self._try_to_upload_logs:
+            self._try_upload_logs()
+            self._try_to_upload_logs = False
+
+          if wifi.acs():
+            wifi.last_successful_bss_info = getattr(wifi,
+                                                    'last_attempted_bss_info',
+                                                    None)
           now = time.time()
           if (self._wlan_configuration and
               hasattr(wifi, 'waiting_for_acs_since')):
@@ -489,8 +526,21 @@
         # If we didn't manage to join the WLAN and we don't have an ACS
         # connection, we should try to establish one.
         else:
-          logging.debug('Not connected to ACS on %s', wifi.name)
-          self._try_next_bssid(wifi)
+          # If we are associated but waiting for a DHCP lease, try again later.
+          now = time.time()
+          connected_to_open = (
+              wifi.wpa_status().get('wpa_state', None) == 'COMPLETED' and
+              wifi.wpa_status().get('key_mgmt', None) == 'NONE')
+          wait_for_dhcp = (
+              not wifi.gateway() and
+              hasattr(wifi, 'waiting_for_dhcp_since') and
+              now - wifi.waiting_for_dhcp_since < self._dhcp_wait_s)
+          if connected_to_open and wait_for_dhcp:
+            logging.debug('Waiting for DHCP lease after %ds.',
+                          now - wifi.waiting_for_acs_since)
+          else:
+            logging.debug('Not connected to ACS')
+            self._try_next_bssid(wifi)
 
     time.sleep(max(0, self._run_duration_s - (time.time() - start_time)))
 
@@ -529,6 +579,37 @@
     self.acs()
     self.internet()
 
+    # Update /etc/hosts (depends on routing table)
+    self._update_tmp_hosts()
+
+  def _update_tmp_hosts(self):
+    """Update the contents of /tmp/hosts."""
+    lowest_metric_interface = None
+    for ifc in [self.bridge] + self.wifi:
+      route = ifc.current_routes().get('default', None)
+      if route:
+        metric = route.get('metric', 0)
+        # Skip temporary connection_check routes.
+        if metric == '99':
+          continue
+        candidate = (metric, ifc)
+        if (lowest_metric_interface is None or
+            candidate < lowest_metric_interface):
+          lowest_metric_interface = candidate
+
+    ip_line = ''
+    if lowest_metric_interface:
+      ip = lowest_metric_interface[1].get_ip_address()
+      ip_line = '%s %s\n' % (ip, HOSTNAME) if ip else ''
+
+    new_tmp_hosts = '%s127.0.0.1 localhost' % ip_line
+
+    if not os.path.exists(TMP_HOSTS) or open(TMP_HOSTS).read() != new_tmp_hosts:
+      tmp_hosts_tmp_filename = TMP_HOSTS + '.tmp'
+      tmp_hosts_tmp = open(tmp_hosts_tmp_filename, 'w')
+      tmp_hosts_tmp.write(new_tmp_hosts)
+      os.rename(tmp_hosts_tmp_filename, TMP_HOSTS)
+
   def handle_event(self, path, filename, deleted):
     if deleted:
       self._handle_deleted_file(path, filename)
@@ -577,7 +658,7 @@
       if filename == self.ETHERNET_STATUS_FILE:
         try:
           self.bridge.ethernet = bool(int(contents))
-          logging.debug('Ethernet %s', 'up' if self.bridge.ethernet else 'down')
+          logging.info('Ethernet %s', 'up' if self.bridge.ethernet else 'down')
         except ValueError:
           logging.error('Status file contents should be 0 or 1, not %s',
                         contents)
@@ -600,7 +681,7 @@
           wifi = self.wifi_for_band(band)
           if wifi and band in self._wlan_configuration:
             self._wlan_configuration[band].access_point = True
-          logging.debug('AP enabled for %s GHz', band)
+          logging.info('AP enabled for %s GHz', band)
 
     elif path == self._tmp_dir:
       if filename.startswith(self.GATEWAY_FILE_PREFIX):
@@ -608,8 +689,16 @@
         ifc = self.interface_by_name(interface_name)
         if ifc:
           ifc.set_gateway_ip(contents)
-          logging.debug('Received gateway %r for interface %s', contents,
-                        ifc.name)
+          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)
@@ -683,16 +772,19 @@
     last_successful_bss_info = getattr(wifi, 'last_successful_bss_info', None)
     bss_info = last_successful_bss_info or wifi.cycler.next()
     if bss_info is not None:
-      logging.debug('Attempting to connect to SSID %s for provisioning',
-                    bss_info.ssid)
+      logging.info('Attempting to connect to SSID %s (%s) for provisioning',
+                   bss_info.ssid, bss_info.bssid)
       self._status.trying_open = True
+      wifi.set_gateway_ip(None)
       connected = self._try_bssid(wifi, bss_info)
       if connected:
         self._status.connected_to_open = True
         now = time.time()
         wifi.waiting_for_acs_since = now
+        wifi.waiting_for_dhcp_since = now
         wifi.complain_about_acs_at = now + 5
         logging.info('Attempting to provision via SSID %s', bss_info.ssid)
+        self._try_to_upload_logs = True
       # If we can no longer connect to this, it's no longer successful.
       elif bss_info == last_successful_bss_info:
         wifi.last_successful_bss_info = None
@@ -734,7 +826,7 @@
         wlan_configuration.access_point = os.path.exists(ap_file)
       self._wlan_configuration[band] = wlan_configuration
       self._status.have_config = True
-      logging.debug('Updated WLAN configuration for %s GHz', band)
+      logging.info('Updated WLAN configuration for %s GHz', band)
       self._update_access_point(wlan_configuration)
 
   def _update_access_point(self, wlan_configuration):
@@ -788,6 +880,11 @@
     subprocess.check_output(self.BINWIFI + list(command),
                             stderr=subprocess.STDOUT)
 
+  def _try_upload_logs(self):
+    logging.info('Attempting to upload logs')
+    if subprocess.call(self.UPLOAD_LOGS_AND_WAIT) != 0:
+      logging.error('Failed to upload logs')
+
 
 def _wifi_show():
   try:
diff --git a/conman/connection_manager_test.py b/conman/connection_manager_test.py
index 954e2e3..271cac7 100755
--- a/conman/connection_manager_test.py
+++ b/conman/connection_manager_test.py
@@ -9,6 +9,7 @@
 import time
 
 import connection_manager
+import experiment_testutils
 import interface_test
 import iw
 import status
@@ -180,22 +181,25 @@
   WIFI_SETCLIENT = ['echo', 'setclient']
   WIFI_STOPCLIENT = ['echo', 'stopclient']
 
-  def start_client(self):
-    client_was_up = self.client_up
-    was_attached = self.wifi.attached()
+  def _actually_start_client(self):
+    self.client_was_up = self.client_up
+    self.was_attached = self.wifi.attached()
+    self.wifi._secure_testonly = True
     # Do this before calling the super method so that the attach call at the end
     # succeeds.
-    if not client_was_up and not was_attached:
+    if not self.client_was_up and not self.was_attached:
       self.wifi._initial_ssid_testonly = self.ssid
       self.wifi.start_wpa_supplicant_testonly(self._wpa_control_interface)
 
-    super(WLANConfiguration, self).start_client()
+    return True
 
-    if not client_was_up:
+  def _post_start_client(self):
+    if not self.client_was_up:
       self.wifi.set_connection_check_result('succeed')
 
-      if was_attached:
+      if self.was_attached:
         self.wifi._wpa_control.ssid_testonly = self.ssid
+        self.wifi._wpa_control.secure_testonly = True
         self.wifi.add_connected_event()
 
       # Normally, wpa_supplicant would bring up the client interface, which
@@ -204,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
@@ -229,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:
@@ -263,6 +275,7 @@
   IFUP = ['echo', 'ifup']
   IFPLUGD_ACTION = ['echo', 'ifplugd.action']
   BINWIFI = ['echo', 'wifi']
+  UPLOAD_LOGS_AND_WAIT = ['echo', 'upload-logs-and-wait']
 
   def __init__(self, *args, **kwargs):
     self._binwifi_commands = []
@@ -295,12 +308,16 @@
     self.can_connect_to_s3 = True
     # Will s2 fail rather than providing ACS access?
     self.s2_fail = False
+    # Will s3 fail to acquire a DHCP lease?
+    self.dhcp_failure_on_s3 = False
+    self.log_upload_count = 0
 
   def create_wifi_interfaces(self):
     super(ConnectionManager, self).create_wifi_interfaces()
     for wifi in self.wifi_interfaces_already_up:
       # pylint: disable=protected-access
       self.interface_by_name(wifi)._initial_ssid_testonly = 'my ssid'
+      self.interface_by_name(wifi)._secure_testonly = True
 
   @property
   def IP_LINK(self):
@@ -316,20 +333,23 @@
         wifi.add_terminating_event()
 
   def _try_bssid(self, wifi, bss_info):
+    wifi.add_disconnected_event()
     self.last_provisioning_attempt = bss_info
 
     super(ConnectionManager, self)._try_bssid(wifi, bss_info)
 
-    def connect(connection_check_result):
+    def connect(connection_check_result, dhcp_failure=False):
       # pylint: disable=protected-access
       if wifi.attached():
-        wifi._wpa_control._ssid_testonly = bss_info.ssid
+        wifi._wpa_control.ssid_testonly = bss_info.ssid
+        wifi._wpa_control.secure_testonly = False
         wifi.add_connected_event()
       else:
         wifi._initial_ssid_testonly = bss_info.ssid
+        wifi._secure_testonly = False
         wifi.start_wpa_supplicant_testonly(self._wpa_control_interface)
       wifi.set_connection_check_result(connection_check_result)
-      self.ifplugd_action(wifi.name, True)
+      self.ifplugd_action(wifi.name, True, dhcp_failure)
 
     if bss_info and bss_info.ssid == 's1':
       connect('fail')
@@ -340,7 +360,7 @@
       return True
 
     if bss_info and bss_info.ssid == 's3' and self.can_connect_to_s3:
-      connect('restricted')
+      connect('restricted', self.dhcp_failure_on_s3)
       return True
 
     return False
@@ -362,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)
@@ -372,16 +393,18 @@
     super(ConnectionManager, self)._wifi_scan(wifi)
     wifi.wifi_scan_counter += 1
 
-  def ifplugd_action(self, interface_name, up):
+  def ifplugd_action(self, interface_name, up, dhcp_failure=False):
     # Typically, when moca comes up, conman calls ifplugd.action, which writes
     # this file.  Also, when conman starts, it calls ifplugd.action for eth0.
     self.write_interface_status_file(interface_name, '1' if up else '0')
 
     # ifplugd calls run-dhclient, which results in a gateway file if the link is
     # up (and working).
-    if up:
+    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)
@@ -401,6 +424,10 @@
 
     return self._wlan_configuration[band].client_up
 
+  def _try_upload_logs(self):
+    self.log_upload_count += 1
+    return super(ConnectionManager, self)._try_upload_logs()
+
   # Test methods
 
   def delete_wlan_config(self, band):
@@ -422,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:
@@ -493,6 +527,10 @@
     os.unlink(ap_filename)
 
 
+def check_tmp_hosts(expected_contents):
+  wvtest.WVPASSEQ(open(connection_manager.TMP_HOSTS).read(), expected_contents)
+
+
 def connection_manager_test(radio_config, wlan_configs=None,
                             quantenna_interfaces=None, **cm_kwargs):
   """Returns a decorator that does ConnectionManager test boilerplate."""
@@ -507,6 +545,7 @@
       interface_update_period = 5
       wifi_scan_period = 15
       wifi_scan_period_s = run_duration_s * wifi_scan_period
+      dhcp_wait_s = .5
 
       # pylint: disable=protected-access
       old_wifi_show = connection_manager._wifi_show
@@ -518,6 +557,7 @@
 
       try:
         # No initial state.
+        connection_manager.TMP_HOSTS = tempfile.mktemp()
         tmp_dir = tempfile.mkdtemp()
         config_dir = tempfile.mkdtemp()
         os.mkdir(os.path.join(tmp_dir, 'interfaces'))
@@ -540,14 +580,18 @@
                               run_duration_s=run_duration_s,
                               interface_update_period=interface_update_period,
                               wifi_scan_period_s=wifi_scan_period_s,
-                              bssid_cycle_length_s=0.05,
+                              dhcp_wait_s=dhcp_wait_s,
+                              bssid_cycle_length_s=1,
                               **cm_kwargs)
 
         c.test_interface_update_period = interface_update_period
         c.test_wifi_scan_period = wifi_scan_period
+        c.test_dhcp_wait_s = dhcp_wait_s
 
         f(c)
       finally:
+        if os.path.exists(connection_manager.TMP_HOSTS):
+          os.unlink(connection_manager.TMP_HOSTS)
         shutil.rmtree(tmp_dir)
         shutil.rmtree(config_dir)
         shutil.rmtree(moca_tmp_dir)
@@ -585,14 +629,15 @@
   wvtest.WVPASS(c.internet())
   wvtest.WVPASS(c.has_status_files([status.P.CAN_REACH_ACS,
                                     status.P.CAN_REACH_INTERNET]))
+  hostname = connection_manager.HOSTNAME
 
   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]))
 
@@ -601,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]))
@@ -611,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)
@@ -647,13 +692,18 @@
   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.
   c.interface_with_scan_results = c.wifi_for_band(band).name
   # Wait for a scan, plus 3 cycles, so that s2 will have been tried.
   c.run_until_scan(band)
-  for _ in range(3):
+  wvtest.WVPASSEQ(c.log_upload_count, 0)
+  c.wifi_for_band(band).ip_testonly = '192.168.1.100'
+  for _ in range(len(c.wifi_for_band(band).cycler)):
     c.run_once()
     wvtest.WVPASS(c.has_status_files([status.P.CONNECTED_TO_OPEN]))
 
@@ -666,9 +716,12 @@
   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
+  c.run_until_interface_update()
+  check_tmp_hosts('192.168.1.100 %s\n127.0.0.1 localhost' % hostname)
 
   # Now, create a WLAN configuration which should be connected to.
   ssid = 'wlan'
@@ -677,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.
@@ -706,6 +759,9 @@
   wvtest.WVPASS(c.has_status_files([status.P.CONNECTED_TO_OPEN]))
   wvtest.WVPASSEQ(c.last_provisioning_attempt.ssid, 's3')
   wvtest.WVPASSEQ(c.last_provisioning_attempt.bssid, 'ff:ee:dd:cc:bb:aa')
+  # The log upload happens on the next main loop after joining s3.
+  c.run_once()
+  wvtest.WVPASSEQ(c.log_upload_count, 2)
 
   # Now, recreate the same WLAN configuration, which should be connected to.
   # Also, test that atomic writes/renames work.
@@ -715,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
@@ -723,18 +779,22 @@
   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)
 
   # Now bring up the bridge.  We should remove the wifi connection and start
   # an AP.
   c.set_ethernet(True)
   c.bridge.set_connection_check_result('succeed')
+  c.bridge.ip_testonly = '192.168.1.101'
   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
   # away, and we should not be able to join the WLAN.  Routes should not be
@@ -745,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.
@@ -754,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.
@@ -765,6 +825,9 @@
   c.run_until_interface_update()
   wvtest.WVFAIL(c.acs())
   wvtest.WVFAIL(c.internet())
+  # 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,
@@ -773,6 +836,8 @@
   wvtest.WVPASS(c.acs())
   # Make sure we didn't scan on `band`.
   wvtest.WVPASSEQ(scan_count_for_band, c.wifi_for_band(band).wifi_scan_counter)
+  c.run_once()
+  wvtest.WVPASSEQ(c.log_upload_count, 3)
 
   # Now re-create the WLAN config, connect to the WLAN, and make sure that s3 is
   # unset as last_successful_bss_info, since it is no longer available.
@@ -815,6 +880,8 @@
     c.run_once()
   s2_bss = iw.BssInfo('01:23:45:67:89:ab', 's2')
   wvtest.WVPASSEQ(c.wifi_for_band(band).last_successful_bss_info, s2_bss)
+  c.run_once()
+  wvtest.WVPASSEQ(c.log_upload_count, 4)
 
   c.s2_fail = True
   c.write_wlan_config(band, ssid, psk)
@@ -830,6 +897,32 @@
   c.run_until_interface_update()
   wvtest.WVPASSEQ(c.wifi_for_band(band).last_successful_bss_info, None)
 
+  # Test that we wait dhcp_wait_s seconds for a DHCP lease before trying the
+  # next BSSID.  The scan results contain an s3 AP with vendor IEs that fails to
+  # send a DHCP lease.  This ensures that s3 will be tried before any other AP,
+  # which lets us force a timeout and proceed to the next AP.
+  del c.wifi_for_band(band).cycler
+  c.interface_with_scan_results = c.wifi_for_band(band).name
+  c.scan_results_include_hidden = True
+  c.can_connect_to_s3 = True
+  c.dhcp_failure_on_s3 = True
+  # First iteration: check that we try s3.
+  c.run_until_scan(band)
+  last_bss_info = c.wifi_for_band(band).last_attempted_bss_info
+  wvtest.WVPASSEQ(last_bss_info.ssid, 's3')
+  wvtest.WVPASSEQ(last_bss_info.bssid, 'ff:ee:dd:cc:bb:aa')
+  # Second iteration: check that we try s3 again since there's no gateway yet.
+  c.run_once()
+  last_bss_info = c.wifi_for_band(band).last_attempted_bss_info
+  wvtest.WVPASSEQ(last_bss_info.ssid, 's3')
+  wvtest.WVPASSEQ(last_bss_info.bssid, 'ff:ee:dd:cc:bb:aa')
+  # Third iteration: sleep for dhcp_wait_s and check that we try another AP.
+  time.sleep(c.test_dhcp_wait_s)
+  c.run_once()
+  last_bss_info = c.wifi_for_band(band).last_attempted_bss_info
+  wvtest.WVPASSNE(last_bss_info.ssid, 's3')
+  wvtest.WVPASSNE(last_bss_info.bssid, 'ff:ee:dd:cc:bb:aa')
+
 
 @wvtest.wvtest
 @connection_manager_test(WIFI_SHOW_OUTPUT_MARVELL8897)
@@ -908,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.
@@ -921,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.
@@ -932,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
@@ -943,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
@@ -964,9 +1057,11 @@
     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)
 
 
 @wvtest.wvtest
@@ -1012,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.
@@ -1023,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')
@@ -1033,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
@@ -1045,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
@@ -1056,9 +1151,11 @@
     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)
 
 
 @wvtest.wvtest
@@ -1101,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
@@ -1114,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
@@ -1144,5 +1241,29 @@
                 in c._binwifi_commands)
 
 
+@wvtest.wvtest
+@connection_manager_test(WIFI_SHOW_OUTPUT_MARVELL8897)
+def connection_manager_conman_no_2g_wlan(c):
+  unused_raii = experiment_testutils.MakeExperimentDirs()
+
+  # First, establish that we connect on 2.4 without the experiment, to make sure
+  # this test doesn't spuriously pass.
+  c.write_wlan_config('2.4', 'my ssid', 'my psk')
+  c.run_once()
+  wvtest.WVPASS(c.client_up('2.4'))
+
+  # Now, force a disconnect by deleting the config.
+  c.delete_wlan_config('2.4')
+  c.run_once()
+  wvtest.WVFAIL(c.client_up('2.4'))
+
+  # Now enable the experiment, recreate the config, and make sure we don't
+  # connect.
+  experiment_testutils.enable('WifiNo2GClient')
+  c.write_wlan_config('2.4', 'my ssid', 'my psk')
+  c.run_once()
+  wvtest.WVFAIL(c.client_up('2.4'))
+
+
 if __name__ == '__main__':
   wvtest.wvtest_main()
diff --git a/conman/interface.py b/conman/interface.py
index 7e37306..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
@@ -28,8 +34,9 @@
 
   CONNECTION_CHECK = 'connection_check'
   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.
@@ -39,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.
 
@@ -57,27 +69,30 @@
     """
     # Until initialized, we want to act as if the interface is down.
     if not self._initialized:
-      logging.debug('%s not initialized; not running connection_check%s',
-                    self.name, ' (ACS)' if check_acs else '')
+      logging.info('%s not initialized; not running connection_check%s',
+                   self.name, ' (ACS)' if check_acs else '')
       return None
 
     if not self.links:
-      logging.debug('Connection check for %s failed due to no links', self.name)
+      logging.info('Connection check for %s failed due to no links', self.name)
       return False
 
     logging.debug('Gateway IP for %s is %s', self.name, self._gateway_ip)
     if self._gateway_ip is None:
-      logging.debug('Connection check for %s failed due to no gateway IP',
-                    self.name)
+      logging.info('Connection check%s for %s failed due to no gateway IP',
+                   ' (ACS)' if check_acs else '', self.name)
       return False
 
     # Temporarily add a route to make sure the connection check can be run.
     # 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,
@@ -90,21 +105,27 @@
 
     with open(os.devnull, 'w') as devnull:
       result = subprocess.call(cmd, stdout=devnull, stderr=devnull) == 0
-      logging.debug('Connection check%s for %s %s',
-                    ' (ACS)' if check_acs else '',
-                    self.name,
-                    'passed' if result else 'failed')
+      logging.info('Connection check%s for %s %s',
+                   ' (ACS)' if check_acs else '',
+                   self.name,
+                   'passed' if result else 'failed')
 
     # 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
 
+  def gateway(self):
+    return self._gateway_ip
+
   def acs(self):
     if self._has_acs is None:
       self._has_acs = self._connection_check(check_acs=True)
@@ -117,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)
@@ -131,52 +151,95 @@
       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
 
   def _ip_route(self, *args):
     if not self._initialized:
-      logging.debug('Not initialized, not running %s %s',
-                    ' '.join(self.IP_ROUTE), ' '.join(args))
+      logging.info('Not initialized, not running %s %s',
+                   ' '.join(self.IP_ROUTE), ' '.join(args))
       return ''
 
     return self._really_ip_route(*args)
@@ -190,10 +253,27 @@
                     e.message)
       return ''
 
+  def _ip_addr_show(self):
+    try:
+      return subprocess.check_output(self.IP_ADDR_SHOW + [self.name])
+    except subprocess.CalledProcessError as e:
+      logging.error('Could not get IP address for %s: %s', self.name, e.message)
+      return None
+
+  def get_ip_address(self):
+    match = re.search(r'^\s*inet (?P<IP>\d+\.\d+\.\d+\.\d+)',
+                      self._ip_addr_show(), re.MULTILINE)
+    return match and match.group('IP') or None
+
   def set_gateway_ip(self, gateway_ip):
-    logging.debug('New gateway IP %s for %s', gateway_ip, self.name)
+    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."""
@@ -203,10 +283,10 @@
     had_links = bool(self.links)
 
     if is_up:
-      logging.debug('%s gained link %s', self.name, link)
+      logging.info('%s gained link %s', self.name, link)
       self.links.add(link)
     else:
-      logging.debug('%s lost link %s', self.name, link)
+      logging.info('%s lost link %s', self.name, link)
       self.links.remove(link)
 
     # If a link goes away, we may have lost access to something but not gained
@@ -227,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.
@@ -303,25 +398,31 @@
       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."""
     failure_s = self._acs_session_failure_s()
     if (experiment.enabled('WifiSimulateWireless')
         and failure_s < MAX_ACS_FAILURE_S):
-      logging.debug('WifiSimulateWireless: failing bridge connection check (no '
-                    'ACS contact for %d seconds, max %d seconds)',
-                    failure_s, MAX_ACS_FAILURE_S)
+      logging.info('WifiSimulateWireless: failing bridge connection check%s '
+                   '(no ACS contact for %d seconds, max %d seconds)',
+                   ' (ACS)' if check_acs else '', failure_s, MAX_ACS_FAILURE_S)
       return False
 
     return super(Bridge, self)._connection_check(check_acs)
@@ -379,14 +480,17 @@
       return True
 
     socket = os.path.join(path, self.name)
+    logging.debug('%s socket is %s', self.name, socket)
     try:
       self._wpa_control = self.get_wpa_control(socket)
       self._wpa_control.attach()
+      logging.debug('%s successfully attached', self.name)
     except wpactrl.error as e:
       logging.error('Error attaching to wpa_supplicant: %s', e)
       return False
 
     status = self.wpa_status()
+    logging.debug('%s status after attaching is %s', self.name, status)
     self.wpa_supplicant = status.get('wpa_state') == 'COMPLETED'
     if not self._initialized:
       self.initial_ssid = status.get('ssid')
@@ -403,17 +507,21 @@
     status = {}
 
     if self._wpa_control and self._wpa_control.attached:
+      logging.debug('%s ctrl_iface_path %s',
+                    self, self._wpa_control.ctrl_iface_path)
       lines = []
       try:
         lines = self._wpa_control.request('STATUS').splitlines()
-      except wpactrl.error:
-        logging.error('wpa_control STATUS request failed')
+      except wpactrl.error as e:
+        logging.error('wpa_control STATUS request failed %s args %s',
+                      e.message, e.args)
       for line in lines:
         if '=' not in line:
           continue
         k, v = line.strip().split('=', 1)
         status[k] = v
 
+    logging.debug('%s wpa status is %s', self.name, status)
     return status
 
   def get_wpa_control(self, socket):
@@ -468,17 +576,22 @@
   WIFIINFO_PATH = '/tmp/wifi/wifiinfo'
 
   def __init__(self, socket):
-    self._interface = os.path.split(socket)[-1]
+    self.ctrl_iface_path, self._interface = os.path.split(socket)
 
     # State from QCSAPI and wifi_files.
     self._client_mode = False
     self._ssid = None
     self._status = None
+    self._security = None
 
     self._events = []
 
   def _qcsapi(self, *command):
-    return subprocess.check_output(['qcsapi'] + list(command)).strip()
+    try:
+      return subprocess.check_output(['qcsapi'] + list(command)).strip()
+    except subprocess.CalledProcessError as e:
+      logging.error('QCSAPI call failed: %s: %s', e, e.output)
+      raise
 
   def attach(self):
     self._update()
@@ -500,6 +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)
+                  if ssid else None)
     except subprocess.CalledProcessError:
       # If QCSAPI failed, skip update.
       return
@@ -529,6 +644,7 @@
     self._client_mode = client_mode
     self._ssid = ssid
     self._status = status
+    self._security = security
 
   def recv(self):
     return self._events.pop(0)
@@ -544,7 +660,8 @@
     if not self._client_mode or not self._ssid:
       return ''
 
-    return 'wpa_state=COMPLETED\nssid=%s' % self._ssid
+    return ('wpa_state=COMPLETED\nssid=%s\nkey_mgmt=%s' %
+            (self._ssid, self._security or 'NONE'))
 
 
 class FrenzyWifi(Wifi):
diff --git a/conman/interface_test.py b/conman/interface_test.py
index 4c7d52b..e8ab4ca 100755
--- a/conman/interface_test.py
+++ b/conman/interface_test.py
@@ -22,6 +22,13 @@
 from wvtest import wvtest
 
 
+# pylint: disable=line-too-long
+_IP_ADDR_SHOW_TPL = """4: {name}: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP qlen 1000
+    inet {ip}/21 brd 100.100.55.255 scope global {name}
+       valid_lft forever preferred_lft forever
+"""
+
+
 class FakeInterfaceMixin(object):
   """Replace Interface methods which interact with the system."""
 
@@ -29,6 +36,7 @@
     super(FakeInterfaceMixin, self).__init__(*args, **kwargs)
     self.set_connection_check_result('succeed')
     self.routing_table = {}
+    self.ip_testonly = None
 
   def set_connection_check_result(self, result):
     if result in ['succeed', 'fail', 'restricted']:
@@ -40,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:])
@@ -55,14 +67,24 @@
       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
 
+  def _ip_addr_show(self):
+    if self.ip_testonly:
+      return _IP_ADDR_SHOW_TPL.format(name=self.name, ip=self.ip_testonly)
+
+    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
@@ -78,6 +100,7 @@
     self.attached = False
     self.connected = False
     self.ssid_testonly = None
+    self.secure_testonly = False
     self.request_status_fails = False
 
   def pending(self):
@@ -96,6 +119,7 @@
   def detach(self):
     self.attached = False
     self.ssid_testonly = None
+    self.secure_testonly = False
     self.connected = False
     self.check_socket_exists('wpactrl_detach failed')
 
@@ -103,11 +127,19 @@
     if request_type == 'STATUS':
       if self.request_status_fails:
         raise wpactrl.error('test error')
-      return ('foo\nwpa_state=COMPLETED\nssid=%s\nbar' % self.ssid_testonly
-              if self.connected else 'foo')
+      if self.connected:
+        return ('foo\nwpa_state=COMPLETED\nssid=%s\nkey_mgmt=%s\nbar' %
+                (self.ssid_testonly,
+                 'WPA2-PSK' if self.secure_testonly else 'NONE'))
+      else:
+        return 'wpa_state=SCANNING\naddress=12:34:56:78:90:ab'
     else:
       raise ValueError('Invalid request_type %s' % request_type)
 
+  @property
+  def ctrl_iface_path(self):
+    return os.path.split(self._socket)[0]
+
   # Below methods are not part of WPACtrl.
 
   def add_event(self, event):
@@ -142,6 +174,7 @@
   def __init__(self, *args, **kwargs):
     super(Wifi, self).__init__(*args, **kwargs)
     self._initial_ssid_testonly = None
+    self._secure_testonly = False
 
   def attach_wpa_control(self, path):
     if self._initial_ssid_testonly and self._wpa_control:
@@ -153,6 +186,7 @@
     if self._initial_ssid_testonly:
       result.connected = True
       result.ssid_testonly = self._initial_ssid_testonly
+      result.secure_testonly = self._secure_testonly
     return result
 
   def add_connected_event(self):
@@ -161,16 +195,19 @@
 
   def add_disconnected_event(self):
     self._initial_ssid_testonly = None
+    self._secure_testonly = False
     if self.attached():
       self._wpa_control.add_disconnected_event()
 
   def add_terminating_event(self):
     self._initial_ssid_testonly = None
+    self._secure_testonly = False
     if self.attached():
       self._wpa_control.add_terminating_event()
 
   def detach_wpa_control(self):
     self._initial_ssid_testonly = None
+    self._secure_testonly = False
     super(Wifi, self).detach_wpa_control()
 
   def start_wpa_supplicant_testonly(self, path):
@@ -191,6 +228,7 @@
   def __init__(self, *args, **kwargs):
     super(FrenzyWPACtrl, self).__init__(*args, **kwargs)
     self.ssid_testonly = None
+    self.secure_testonly = False
     self.request_status_fails = False
 
   def _qcsapi(self, *command):
@@ -199,15 +237,21 @@
   def add_connected_event(self):
     self.fake_qcsapi['get_mode'] = 'Station'
     self.fake_qcsapi['get_ssid'] = self.ssid_testonly
+    security = 'PSKAuthentication' if self.secure_testonly else 'NONE'
+    self.fake_qcsapi['ssid_get_authentication_mode'] = security
 
   def add_disconnected_event(self):
     self.ssid_testonly = None
+    self.secure_testonly = False
     self.fake_qcsapi['get_ssid'] = None
+    self.fake_qcsapi['ssid_get_authentication_mode'] = 'NONE'
 
   def add_terminating_event(self):
     self.ssid_testonly = None
+    self.secure_testonly = False
     self.fake_qcsapi['get_ssid'] = None
     self.fake_qcsapi['get_mode'] = 'AP'
+    self.fake_qcsapi['ssid_get_authentication_mode'] = 'NONE'
 
   def detach(self):
     self.add_terminating_event()
@@ -226,12 +270,14 @@
   def __init__(self, *args, **kwargs):
     super(FrenzyWifi, self).__init__(*args, **kwargs)
     self._initial_ssid_testonly = None
+    self._secure_testonly = False
     self.fake_qcsapi = {}
 
   def attach_wpa_control(self, *args, **kwargs):
     super(FrenzyWifi, self).attach_wpa_control(*args, **kwargs)
     if self._wpa_control:
       self._wpa_control.ssid_testonly = self._initial_ssid_testonly
+      self._wpa_control.secure_testonly = self._secure_testonly
       if self._initial_ssid_testonly:
         self._wpa_control.add_connected_event()
 
@@ -241,6 +287,7 @@
     if self._initial_ssid_testonly:
       result.fake_qcsapi['get_mode'] = 'Station'
       result.ssid_testonly = self._initial_ssid_testonly
+      result.secure_testonly = self._secure_testonly
       result.add_connected_event()
     return result
 
@@ -250,16 +297,19 @@
 
   def add_disconnected_event(self):
     self._initial_ssid_testonly = None
+    self._secure_testonly = False
     if self.attached():
       self._wpa_control.add_disconnected_event()
 
   def add_terminating_event(self):
     self._initial_ssid_testonly = None
+    self._secure_testonly = False
     if self.attached():
       self._wpa_control.add_terminating_event()
 
   def detach_wpa_control(self):
     self._initial_ssid_testonly = None
+    self._secure_testonly = False
     super(FrenzyWifi, self).detach_wpa_control()
 
   def start_wpa_supplicant_testonly(self, unused_path):
@@ -288,55 +338,71 @@
 
     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())
+    b.ip_testonly = '192.168.1.100'
+    wvtest.WVPASSEQ(b.get_ip_address(), '192.168.1.100')
+
   finally:
     shutil.rmtree(tmp_dir)
 
diff --git a/conman/iw.py b/conman/iw.py
index fd56e32..f2e15d8 100755
--- a/conman/iw.py
+++ b/conman/iw.py
@@ -6,19 +6,10 @@
 import subprocess
 
 
-FIBER_OUI = 'f4:f5:e8'
-DEFAULT_GFIBERSETUP_SSID = 'GFiberSetupAutomation'
-
-
-def _scan(band, **kwargs):
-  try:
-    return subprocess.check_output(('wifi', 'scan', '-b', band), **kwargs)
-  except subprocess.CalledProcessError:
-    return ''
-
-
-GFIBER_OUIS = ['f4:f5:e8']
+GFIBER_VENDOR_IE_OUI = 'f4:f5:e8'
+GFIBER_OUIS = ['00:1a:11', 'f4:f5:e8', 'f8:8f:ca']
 VENDOR_IE_FEATURE_ID_AUTOPROVISIONING = '01'
+DEFAULT_GFIBERSETUP_SSID = 'GFiberSetupAutomation'
 
 
 _BSSID_RE = r'BSS (?P<BSSID>([0-9a-f]{2}:?){6})\(on .*\)'
@@ -28,6 +19,13 @@
                  'data:(?P<data>( [0-9a-f]{2})+)')
 
 
+def _scan(band, **kwargs):
+  try:
+    return subprocess.check_output(('wifi', 'scan', '-b', band), **kwargs)
+  except subprocess.CalledProcessError:
+    return ''
+
+
 class BssInfo(object):
   """Contains info about a BSS, parsed from 'iw scan'."""
 
@@ -119,7 +117,7 @@
       continue
 
     for oui, data in bss_info.vendor_ies:
-      if oui == FIBER_OUI:
+      if oui == GFIBER_VENDOR_IE_OUI:
         octets = data.split()
         if octets[0] == '03' and not bss_info.ssid:
           bss_info.ssid = ''.join(octets[1:]).decode('hex')
@@ -138,7 +136,7 @@
 def _bssid_priority(bss_info):
   result = 4 if bss_info.bssid[:8] in GFIBER_OUIS else 2
   for oui, data in bss_info.vendor_ies:
-    if (oui in GFIBER_OUIS and
+    if (oui == GFIBER_VENDOR_IE_OUI and
         data.startswith(VENDOR_IE_FEATURE_ID_AUTOPROVISIONING)):
       result = 5
 
diff --git a/conman/iw_test.py b/conman/iw_test.py
index 3bb3cf7..55b2e7b 100755
--- a/conman/iw_test.py
+++ b/conman/iw_test.py
@@ -553,7 +553,7 @@
   Vendor specific: OUI 00:11:22, data: 01 23 45 67
   Vendor specific: OUI f4:f5:e8, data: 01
   Vendor specific: OUI f4:f5:e8, data: 03 47 46 69 62 65 72 53 65 74 75 70 41 75 74 6f 6d 61 74 69 6f 6e
-BSS f4:f5:e8:f1:36:43(on wcli0)
+BSS 00:1a:11:f1:36:43(on wcli0)
   TSF: 12499150000 usec (0d, 03:28:19)
   freq: 2437
   beacon interval: 100 TUs
@@ -646,7 +646,7 @@
                                      vendor_ies=[test_ie, provisioning_ie,
                                                  ssid_ie])
   provisioning_bss_info_frenzy = iw.BssInfo(ssid=iw.DEFAULT_GFIBERSETUP_SSID,
-                                            bssid='f4:f5:e8:f1:36:43',
+                                            bssid='00:1a:11:f1:36:43',
                                             rssi=-66)
 
   wvtest.WVPASSEQ(
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/craftui/craftui b/craftui/craftui
index 9d2a17a..2250595 100755
--- a/craftui/craftui
+++ b/craftui/craftui
@@ -3,6 +3,8 @@
 pycode=/bin/craftui.py
 cw=/usr/catawampus
 devcw=../../../../vendor/google/catawampus
+tornado=
+devtornado=../../../../vendor/opensource/tornado
 localwww=./www
 
 # in developer environment if vendor/google/catawapus is above us
@@ -18,6 +20,7 @@
 # if running from developer desktop, use simulated data
 if [ -n "$sim" ]; then
   cw="$devcw"
+  tornado="$devtornado"
   args="$args --http-port=$((8888+2*($sim-1)))"
   args="$args --https-port=$((8889+2*($sim-1)))"
   args="$args --sim=./sim$sim"
@@ -43,5 +46,5 @@
   exit 1
 done
 
-export PYTHONPATH="$cw/tr/vendor/tornado:$cw/tr/vendor/curtain:$PYTHONPATH"
+export PYTHONPATH="$tornado:$cw/tr/vendor/curtain:$PYTHONPATH"
 exec python -u $debug $pycode $args $httpsmode
diff --git a/gpio-mailbox/Makefile b/gpio-mailbox/Makefile
index 1dc26a4..eaaee20 100644
--- a/gpio-mailbox/Makefile
+++ b/gpio-mailbox/Makefile
@@ -29,8 +29,6 @@
   CFLAGS += -DGFIBER_LT
 else ifeq ($(BR2_TARGET_GENERIC_PLATFORM_NAME),gflt200)
   CFLAGS += -DGFIBER_LT
-else ifeq ($(BR2_TARGET_GENERIC_PLATFORM_NAME),gflt300)
-  CFLAGS += -DGFIBER_LT
 else ifeq ($(BR2_TARGET_GENERIC_PLATFORM_NAME),gfmn100)
   CFLAGS += -DWINDCHARGER
 else ifeq ($(BR2_TARGET_GENERIC_PLATFORM_NAME),gfch100)
diff --git a/gpio-mailbox/TEST.gpio-mailbox b/gpio-mailbox/TEST.gpio-mailbox
index 3cd5dee..edc8f60 100644
--- a/gpio-mailbox/TEST.gpio-mailbox
+++ b/gpio-mailbox/TEST.gpio-mailbox
@@ -1,4 +1,4 @@
-rm -rf /tmp/gpio /tmp/led
+rm -rf /tmp/gpio /tmp/leds
 
 mkdir -p /tmp/gpio
 echo x5 0 1 0 2 0 0x0f > /tmp/gpio/leds
diff --git a/gpio-mailbox/broadcom.c b/gpio-mailbox/broadcom.c
index 575f197..a53e51e 100644
--- a/gpio-mailbox/broadcom.c
+++ b/gpio-mailbox/broadcom.c
@@ -614,6 +614,15 @@
   }
 }
 
+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
+}
+
 static int platform_init(struct platform_info* p) {
   platform_cleanup();
 
@@ -623,8 +632,8 @@
     return -1;
   }
   mmap_size = p->mmap_size;
-  mmap_addr = mmap(NULL, mmap_size, PROT_READ | PROT_WRITE, MAP_SHARED,
-                   mmap_fd, p->mmap_base);
+  mmap_addr = mmap_(NULL, mmap_size, PROT_READ | PROT_WRITE, MAP_SHARED,
+                    mmap_fd, p->mmap_base);
   if (mmap_addr == MAP_FAILED) {
     perror("mmap");
     platform_cleanup();
diff --git a/gpio-mailbox/gfch100.c b/gpio-mailbox/gfch100.c
index 73d97bd..2cf1f23 100644
--- a/gpio-mailbox/gfch100.c
+++ b/gpio-mailbox/gfch100.c
@@ -18,15 +18,13 @@
 #define GPIO_OUT                "out"
 
 /* GPIO_ACTIVITY LED is blue on Chimera. */
-#define GPIO_ACTIVITY           "30"
-#define GPIO_RED                "31"
+#define GPIO_ACTIVITY		"/led_activity"
+#define GPIO_RED		"/led_red"
 
-#define GPIO_BASE_DIR           "/sys/class/gpio"
-#define GPIO_EXPORT             GPIO_BASE_DIR "/export"
+#define GPIO_BASE_DIR		"/dev/gpio"
 
-#define GPIO_DIR(n)             GPIO_BASE_DIR "/gpio" n
+#define GPIO_DIR(n)             GPIO_BASE_DIR n
 
-#define GPIO_DIRECTION(dir)     dir "/direction"
 #define GPIO_VALUE(dir)         dir "/value"
 
 struct PinHandle_s {
@@ -38,9 +36,7 @@
 };
 
 struct sysgpio {
-  const char* export_value;
   const char* value_path;
-  const char* direction_path;
 };
 
 struct platform_info {
@@ -57,13 +53,9 @@
       .value_path = "/sys/class/hwmon/hwmon0/temp1_input",
     },
     .led_red = {
-      .export_value = GPIO_RED,
-      .direction_path = GPIO_DIRECTION(GPIO_DIR(GPIO_RED)),
       .value_path = GPIO_VALUE(GPIO_DIR(GPIO_RED)),
     },
     .led_activity = {
-      .export_value = GPIO_ACTIVITY,
-      .direction_path = GPIO_DIRECTION(GPIO_DIR(GPIO_ACTIVITY)),
       .value_path = GPIO_VALUE(GPIO_DIR(GPIO_ACTIVITY)),
     },
   }
@@ -89,16 +81,6 @@
     perror("calloc(PinHandle)");
     return NULL;
   }
-
-  // initialize leds to match boot values
-  write_file_string(GPIO_EXPORT, GPIO_RED);
-  write_file_string(platform->led_red.direction_path, GPIO_OUT);
-  write_file_string(platform->led_red.value_path, GPIO_OFF);
-
-  write_file_string(GPIO_EXPORT, GPIO_ACTIVITY);
-  write_file_string(platform->led_activity.direction_path, GPIO_OUT);
-  write_file_string(platform->led_activity.value_path, GPIO_ON);
-
   return handle;
 }
 
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/jsonpoll/jsonpoll.py b/jsonpoll/jsonpoll.py
index 11f9c64..cc38875 100755
--- a/jsonpoll/jsonpoll.py
+++ b/jsonpoll/jsonpoll.py
@@ -87,9 +87,8 @@
   def WriteToStderr(self, msg, is_json=False):
     """Write a message to stderr."""
     if is_json:
-      json_data = json.loads(msg)
       flat_data = []
-      self._FlatObject('', json_data, flat_data)
+      self._FlatObject('', msg, flat_data)
       # Make the json easier to parse from the logs.
       for s in flat_data:
         sys.stderr.write('%s\n' % s)
@@ -118,7 +117,7 @@
                                os.path.dirname(output_file))
             continue
           tmpfile = fd.name
-          fd.write(response)
+          fd.write(json.dumps(response))
           fd.flush()
           os.fsync(fd.fileno())
           try:
@@ -131,11 +130,22 @@
         if os.path.exists(tmpfile):
           os.unlink(tmpfile)
 
+  def ParseJSONFromResponse(self, response):
+    try:
+      json_resp = json.loads(response)
+    except UnicodeDecodeError as ex:
+      self.WriteToStderr('Non-UTF8 character in HTTP response: %s', ex)
+      return None
+    except ValueError as ex:
+      self.WriteToStderr('Failed to parse JSON from HTTP response: %s', ex)
+      return None
+    return json_resp
+
   def GetHttpResponse(self, url):
     """Creates a request and retrieves the response from a web server."""
     try:
       handle = urllib2.urlopen(url, timeout=self._SOCKET_TIMEOUT_SECS)
-      response = handle.read()
+      response = self.ParseJSONFromResponse(handle.read())
     except socket.timeout as ex:
       self.WriteToStderr('Connection to %s timed out after %d seconds: %s\n'
                          % (url, self._SOCKET_TIMEOUT_SECS, ex))
@@ -143,9 +153,11 @@
     except urllib2.URLError as ex:
       self.WriteToStderr('Connection to %s failed: %s\n' % (url, ex.reason))
       return None
+
     # Write the response to stderr so it will be uploaded with the other system
     # log files. This will allow turbogrinder to alert on the radio subsystem.
-    self.WriteToStderr(response, is_json=True)
+    if response is not None:
+      self.WriteToStderr(response, is_json=True)
     return response
 
   def CreateDirs(self, dir_to_create):
diff --git a/jsonpoll/jsonpoll_test.py b/jsonpoll/jsonpoll_test.py
index f4f0240..1ccd764 100644
--- a/jsonpoll/jsonpoll_test.py
+++ b/jsonpoll/jsonpoll_test.py
@@ -56,7 +56,7 @@
     self.get_response_called = True
     if self.generate_empty_response:
       return None
-    return json.dumps(JSON_RESPONSE)
+    return self.ParseJSONFromResponse(self.json_response)
 
 
 class JsonPollTest(unittest.TestCase):
@@ -73,6 +73,7 @@
   def setUp(self):
     self.CreateTempFile()
     self.poller = FakeJsonPoll('fakehost.blah', 31337, 1)
+    self.poller.json_response = json.dumps(JSON_RESPONSE)
     self.poller.error_count = 0
     self.poller.generate_empty_response = False
 
@@ -90,7 +91,7 @@
     # equivalent JSON representation we wrote out from the mock.
     with open(self.output_file, 'r') as f:
       output = ''.join(line.rstrip() for line in f)
-      self.assertEqual(json.dumps(JSON_RESPONSE), output)
+      self.assertEqual(JSON_RESPONSE, json.loads(output))
 
   def testRequestStatsFailureToCreateDirOutput(self):
     self.poller.paths_to_statfiles = {'fake/url': '/root/cannotwrite'}
@@ -107,7 +108,7 @@
   def testCachedRequestStats(self):
     # Set the "last_response" as our mock output. This should mean we do not
     # write anything to the output file.
-    self.poller.last_response = json.dumps(JSON_RESPONSE)
+    self.poller.last_response = JSON_RESPONSE
 
     # Create a fake entry in the paths_to_stats map.
     self.poller.paths_to_statfiles = {'fake/url': self.output_file}
@@ -127,5 +128,39 @@
     want = ['base/key1=1', 'base/key2/key3=3', 'base/key2/key4=4']
     self.assertEqual(got.sort(), want.sort())
 
+  def testJSONParsing(self):
+    # { "key": "value" }
+    start_json = ' { "key" : "'
+    euro = u'\u20AC'
+    end_json = '" }'
+
+    # Test for empty JSON
+    self.poller.json_response = ''
+    self.assertEquals(self.poller.GetHttpResponse('fake/url'), None)
+
+    # Test for broken JSON
+    self.poller.json_response = start_json
+    self.assertEquals(self.poller.GetHttpResponse('fake/url'), None)
+    self.poller.json_response = end_json
+    self.assertEquals(self.poller.GetHttpResponse('fake/url'), None)
+    self.poller.json_response = start_json + end_json + end_json
+    self.assertEquals(self.poller.GetHttpResponse('fake/url'), None)
+
+    # The json library (dumps/loads) assumes strings as UTF-8
+    # Need to fail gracefully when wrong encoding is given
+
+    # Normal ascii
+    incoming_json = start_json + 'ascii-value' + end_json
+    self.poller.json_response = incoming_json
+    self.assertNotEquals(self.poller.GetHttpResponse('fake/url'), None)
+
+    # Unicode utf-8: '\xE2 \x82 \xAC' == euro_sign
+    self.poller.json_response = start_json + euro.encode('utf-8') + end_json
+    self.assertNotEquals(self.poller.GetHttpResponse('fake/url'), None)
+
+    # Unicode utf-16: '\x20\xAC' == euro_sign, should fail
+    self.poller.json_response = start_json + euro.encode('utf-16') + end_json
+    self.assertEquals(self.poller.GetHttpResponse('fake/url'), None)
+
 if __name__ == '__main__':
   unittest.main()
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/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/taxonomy/testdata/pcaps/Nexus 5X 2.4GHz Broadcast Probe htcap 01ad.pcap b/taxonomy/testdata/pcaps/Nexus 5X 2.4GHz Broadcast Probe htcap 01ad.pcap
new file mode 100644
index 0000000..e871e93
--- /dev/null
+++ b/taxonomy/testdata/pcaps/Nexus 5X 2.4GHz Broadcast Probe htcap 01ad.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/Nexus 5X 2.4GHz Specific Probe htcap 01ad.pcap b/taxonomy/testdata/pcaps/Nexus 5X 2.4GHz Specific Probe htcap 01ad.pcap
new file mode 100644
index 0000000..85c56e0
--- /dev/null
+++ b/taxonomy/testdata/pcaps/Nexus 5X 2.4GHz Specific Probe htcap 01ad.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/Nexus 5X 5GHz Broadcast Probe htcap 01ad.pcap b/taxonomy/testdata/pcaps/Nexus 5X 5GHz Broadcast Probe htcap 01ad.pcap
new file mode 100644
index 0000000..9a44d6b
--- /dev/null
+++ b/taxonomy/testdata/pcaps/Nexus 5X 5GHz Broadcast Probe htcap 01ad.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/Nexus 5X 5GHz Small Specific Probe.pcap b/taxonomy/testdata/pcaps/Nexus 5X 5GHz Small Specific Probe.pcap
new file mode 100644
index 0000000..f9cfce7
--- /dev/null
+++ b/taxonomy/testdata/pcaps/Nexus 5X 5GHz Small Specific Probe.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/Nexus 5X 5GHz Specific Probe htcap 01ad.pcap b/taxonomy/testdata/pcaps/Nexus 5X 5GHz Specific Probe htcap 01ad.pcap
new file mode 100644
index 0000000..aef7521
--- /dev/null
+++ b/taxonomy/testdata/pcaps/Nexus 5X 5GHz Specific Probe htcap 01ad.pcap
Binary files differ
diff --git a/taxonomy/wifi.py b/taxonomy/wifi.py
index 3445f09..6b48061 100644
--- a/taxonomy/wifi.py
+++ b/taxonomy/wifi.py
@@ -641,6 +641,8 @@
         ('Nexus 5X', '', '5GHz'),
     'wifi4|probe:0,1,127,45,191,htcap:01ef,htagg:03,htmcs:0000ffff,vhtcap:338061b2,vhtrxmcs:030cfffa,vhttxmcs:030cfffa,extcap:00000a020100004080|assoc:0,1,33,36,48,70,45,221(0050f2,2),191,127,htcap:01ef,htagg:03,htmcs:0000ffff,vhtcap:339071b2,vhtrxmcs:030cfffa,vhttxmcs:030cfffa,txpow:1e08,extcap:000000000000004080|oui:lg':
         ('Nexus 5X', '', '5GHz'),
+    'wifi4|probe:0,1,127,45,191,htcap:01ad,htagg:03,htmcs:0000ffff,vhtcap:338061b2,vhtrxmcs:030cfffa,vhttxmcs:030cfffa,extcap:00000a020100004080|assoc:0,1,33,36,48,70,45,221(0050f2,2),191,127,htcap:01ef,htagg:03,htmcs:0000ffff,vhtcap:339071b2,vhtrxmcs:030cfffa,vhttxmcs:030cfffa,txpow:1e08,extcap:000000000000004080|oui:lg':
+        ('Nexus 5X', '', '5GHz'),
     'wifi4|probe:0,1,127,extcap:00000a020100004080|assoc:0,1,33,36,48,70,45,221(0050f2,2),191,127,htcap:01ef,htagg:03,htmcs:0000ffff,vhtcap:339071b2,vhtrxmcs:030cfffa,vhttxmcs:030cfffa,txpow:1e08,extcap:000000000000004080|oui:lg':
         ('Nexus 5X', '', '5GHz'),
     'wifi4|probe:0,1,45,221(0050f2,8),191,127,htcap:01ef,htagg:03,htmcs:0000ffff,vhtcap:339071b2,vhtrxmcs:030cfffa,vhttxmcs:030cfffa,extcap:000000000000004080|assoc:0,1,33,36,48,70,45,221(0050f2,2),191,127,htcap:01ef,htagg:03,htmcs:0000ffff,vhtcap:339071b2,vhtrxmcs:030cfffa,vhttxmcs:030cfffa,txpow:1e08,extcap:000000000000004080|oui:lg':
@@ -653,6 +655,8 @@
         ('Nexus 5X', '', '2.4GHz'),
     'wifi4|probe:0,1,50,127,45,191,htcap:01ef,htagg:03,htmcs:0000ffff,vhtcap:338061b2,vhtrxmcs:030cfffa,vhttxmcs:030cfffa,extcap:00000a020100004080|assoc:0,1,50,33,48,70,45,221(0050f2,2),127,htcap:01ad,htagg:03,htmcs:0000ffff,txpow:1e08,extcap:000000000000000080|oui:lg':
         ('Nexus 5X', '', '2.4GHz'),
+    'wifi4|probe:0,1,50,127,45,191,htcap:01ad,htagg:03,htmcs:0000ffff,vhtcap:338061b2,vhtrxmcs:030cfffa,vhttxmcs:030cfffa,extcap:00000a020100004080|assoc:0,1,50,33,48,70,45,221(0050f2,2),127,htcap:01ad,htagg:03,htmcs:0000ffff,txpow:1e08,extcap:000000000000000080|oui:lg':
+        ('Nexus 5X', '', '2.4GHz'),
     'wifi4|probe:0,1,50,127,extcap:00000a020100004080|assoc:0,1,50,33,48,70,45,221(0050f2,2),127,htcap:01ad,htagg:03,htmcs:0000ffff,txpow:1e08,extcap:000000000000000080|oui:lg':
         ('Nexus 5X', '', '2.4GHz'),
     'wifi4|probe:0,1,50,3,45,221(0050f2,8),127,htcap:01ad,htagg:03,htmcs:0000ffff,extcap:000000000000000080|assoc:0,1,50,33,48,70,45,221(0050f2,2),127,htcap:01ad,htagg:03,htmcs:0000ffff,txpow:1e08,extcap:000000000000000080|oui:lg':
diff --git a/waveguide/clientinfo.py b/waveguide/clientinfo.py
index 28cd643..8173084 100644
--- a/waveguide/clientinfo.py
+++ b/waveguide/clientinfo.py
@@ -30,6 +30,36 @@
   try:
     with open(os.path.join(FINGERPRINTS_DIR, mac)) as f:
       signature = f.read()
-      return ';'.join(taxonomy.identify_wifi_device(signature, mac))
+      (genus, species, perf) = taxonomy.identify_wifi_device(signature, mac)
+
+      # Preserve older output format of chipset;model;performance. We no
+      # longer track chipsets, but we output the leading ';' separator to
+      # maintain compatibility with the format.
+      #
+      # For example, in the old code:
+      # unknown: SHA:c1...7b;Unknown;802.11n n:2,w:40
+      # known:   BCM4329;iPad (1st/2nd gen);802.11n n:1,w:20
+      #
+      # In the current code, in the unknown case:
+      # genus = 'SHA:c1...7b', species = 'Unknown', perf = '802.11n n:2,w:40'
+      # SHA:c1...7b;Unknown;802.11n n:2,w:40
+      #
+      # In the current code, known, with species information:
+      # genus = 'iPad', species = '(1st/2nd gen)', perf = '802.11n n:1,w:20'
+      # ;iPad (1st/2nd gen);802.11n n:1,w:20
+      #
+      # In the current code, known, no specific species:
+      # genus = 'Samsung Galaxy S6', species = '', perf = '802.11ac n:2,w:80'
+      # ;Samsung Galaxy S6;802.11ac n:2,w:80
+      # We don't want an extra space at the end of the model, so we need to be
+      # careful about a join of the empty species.
+      # ;Samsung Galaxy S6 ;802.11ac n:2,w:80
+
+      if genus.startswith('SHA:'):
+        return genus + ';' + species + ';' + perf
+      elif species:
+        return ';' + genus + ' ' + species + ';' + perf
+      else:
+        return ';' + genus + ';' + perf
   except IOError:
     return None
diff --git a/waveguide/log.py b/waveguide/log.py
index af05667..cf9ccb6 100644
--- a/waveguide/log.py
+++ b/waveguide/log.py
@@ -17,15 +17,11 @@
 """Helper functions for logging."""
 
 import errno
-import hmac
 import os
-import struct
 import sys
-import helpers
 
 
 LOGLEVEL = 0
-ANONYMIZE = True
 STATUS_DIR = None
 
 
@@ -47,62 +43,6 @@
     Log(s, *args)
 
 
-SOFT = 'AEIOUY' 'V'
-HARD = 'BCDFGHJKLMNPQRSTVWXYZ' 'AEIOU'
-
-
-def Trigraph(num):
-  """Given a value from 0..4095, encode it as a cons+vowel+cons sequence."""
-  ns = len(SOFT)
-  nh = len(HARD)
-  assert nh * ns * nh >= 4096
-  c3 = num % nh
-  c2 = (num / nh) % ns
-  c1 = num / nh / ns
-  return HARD[c1] + SOFT[c2] + HARD[c3]
-
-
-def WordFromBinary(s):
-  """Encode a binary blob into a string of pronounceable syllables."""
-  out = []
-  while s:
-    part = s[:3]
-    s = s[3:]
-    while len(part) < 4:
-      part = '\0' + part
-    bits = struct.unpack('!I', part)[0]
-    out += [(bits >> 12) & 0xfff,
-            (bits >> 0)  & 0xfff]
-  return ''.join(Trigraph(i) for i in out)
-
-
-# Note(apenwarr): There are a few ways to do this.  I elected to go with
-# short human-usable strings (allowing for the small possibility of
-# collisions) since the log messages will probably be "mostly" used by
-# humans.
-#
-# An alternative would be to use "format preserving encryption" (basically
-# a secure 1:1 mapping of unencrypted to anonymized, in the same number of
-# bits) and then produce longer "words" with no possibility of collision.
-# But with our current WordFromBinary() implementation, that would be
-# 12 characters long, which is kind of inconvenient and we probably don't
-# need that level of care.  Inside waveguide we use the real MAC addresses
-# so collisions won't cause a real problem.
-#
-# TODO(apenwarr): consider not anonymizing the OUI.
-#   That way we could see any behaviour differences between vendors.
-#   Sadly, that might make it too easy to brute force a MAC address back out;
-#   the remaining 3 bytes have too little entropy.
-#
-def AnonymizeMAC(consensus_key, macbin):
-  """Anonymize a binary MAC address using the given key."""
-  assert len(macbin) == 6
-  if consensus_key and ANONYMIZE:
-    return WordFromBinary(hmac.new(consensus_key, macbin).digest())[:6]
-  else:
-    return helpers.DecodeMAC(macbin)
-
-
 def WriteEventFile(name):
   """Create a file in STATUS_DIR if it does not already exist.
 
diff --git a/waveguide/log_test.py b/waveguide/log_test.py
deleted file mode 100755
index fabc09f..0000000
--- a/waveguide/log_test.py
+++ /dev/null
@@ -1,35 +0,0 @@
-#!/usr/bin/python
-import log
-from wvtest import wvtest
-
-
-@wvtest.wvtest
-def AnonTest():
-  m1 = '\x01\x02\x03\x04\x05\x06'
-  m2 = '\x31\x32\x33\x34\x35\x36'
-
-  s1 = log.AnonymizeMAC(None, m1)
-  s2 = log.AnonymizeMAC(None, m2)
-  a1a = log.AnonymizeMAC('key', m1)
-  a2a = log.AnonymizeMAC('key', m2)
-  a1b = log.AnonymizeMAC('key2', m1)
-  a2b = log.AnonymizeMAC('key2', m2)
-
-  # make sure they're printable strings
-  wvtest.WVPASSEQ(s1, str(s1))
-  wvtest.WVPASSEQ(a1a, str(a1a))
-  wvtest.WVPASSEQ(a1b, str(a1b))
-
-  # and reasonably sized
-  wvtest.WVPASSLE(len(a1a), 8)
-
-  # and change when the key or MAC changes
-  wvtest.WVPASSNE(s1, s2)
-  wvtest.WVPASSNE(a1a, a1b)
-  wvtest.WVPASSNE(a2a, a2b)
-  wvtest.WVPASSNE(a1a, a2a)
-  wvtest.WVPASSNE(a1b, a2b)
-
-
-if __name__ == '__main__':
-  wvtest.wvtest_main()
diff --git a/waveguide/waveguide.py b/waveguide/waveguide.py
index 87324c3..594f83f 100755
--- a/waveguide/waveguide.py
+++ b/waveguide/waveguide.py
@@ -58,8 +58,7 @@
 tx-interval=      Seconds between state transmits (0 to disable) [15]
 autochan-interval= Seconds between autochannel decisions (0 to disable) [300]
 print-interval=   Seconds between state printouts to log (0 to disable) [16]
-D,debug           Increase (non-anonymized!) debug output level
-no-anonymize      Don't anonymize MAC addresses in logs
+D,debug           Increase debug output level
 status-dir=       Directory to store status information [/tmp/waveguide]
 watch-pid=        Shut down if the given process pid disappears
 auto-disable-threshold=  Shut down if >= RSSI received from other AP [-30]
@@ -240,11 +239,8 @@
   def Filename(self, suffix):
     return os.path.join(opt.status_dir, '%s.%s' % (self.vdevname, suffix))
 
-  def AnonymizeMAC(self, mac):
-    return log.AnonymizeMAC(consensus_key, mac)
-
   def _LogPrefix(self):
-    return '%s(%s): ' % (self.vdevname, self.AnonymizeMAC(self.mac))
+    return '%s(%s): ' % (self.vdevname, helpers.DecodeMAC(self.mac))
 
   def Log(self, s, *args):
     log.Log(self._LogPrefix() + s, *args)
@@ -291,7 +287,7 @@
       self.Debug('ignoring peer due to key mismatch')
       return 0
     if p.me.mac not in self.peer_list:
-      self.Log('added a peer: %s', self.AnonymizeMAC(p.me.mac))
+      self.Log('added a peer: %s', helpers.DecodeMAC(p.me.mac))
     self.peer_list[p.me.mac] = p
     self.MaybeAutoDisable()
     return 1
@@ -445,7 +441,7 @@
       return None
     for peer in sorted(self.peer_list.values(), key=lambda p: p.me.mac):
       self.Debug('considering auto disable: peer=%s',
-                 self.AnonymizeMAC(peer.me.mac))
+                 helpers.DecodeMAC(peer.me.mac))
       if peer.me.mac not in self.bss_list:
         self.Debug('--> peer no match')
       else:
@@ -478,11 +474,11 @@
     """Writes/removes the auto-disable file based on ShouldAutoDisable()."""
     ad = self.ShouldAutoDisable()
     if ad and self.auto_disabled != ad:
-      self.Log('auto-disabling because of %s', self.AnonymizeMAC(ad))
+      self.Log('auto-disabling because of %s', helpers.DecodeMAC(ad))
       helpers.WriteFileAtomic(self.Filename('disabled'), helpers.DecodeMAC(ad))
     elif self.auto_disabled and not ad:
       self.Log('auto-enabling because %s disappeared',
-               self.AnonymizeMAC(self.auto_disabled))
+               helpers.DecodeMAC(self.auto_disabled))
       helpers.Unlink(self.Filename('disabled'))
     self.auto_disabled = ad
 
@@ -960,17 +956,12 @@
       helpers.WriteFileAtomic(os.path.join(WIFIBLASTER_DIR, g.group()),
                               '%d %s' % (time.time(), line))
 
-  def _AnonymizeResult(self, line):
-    def Repl(match):
-      return log.AnonymizeMAC(consensus_key, helpers.EncodeMAC(match.group()))
-    return re.sub(MACADDR_REGEX, Repl, line)
-
   def _HandleResults(self, errcode, stdout, stderr):
     """Callback for 'wifiblaster' results."""
     log.Debug('wifiblaster err:%r stdout:%r stderr:%r', errcode, stdout[:70],
               stderr)
     for line in stdout.splitlines():
-      log.Log('wifiblaster: %s' % self._AnonymizeResult(line))
+      log.Log('wifiblaster: %s' % line)
       self._SaveResult(line)
 
   def _StrToBool(self, s):
@@ -1090,7 +1081,6 @@
   if opt.watch_pid and opt.watch_pid <= 1:
     o.fatal('--watch-pid must be empty or > 1')
   log.LOGLEVEL = opt.debug
-  log.ANONYMIZE = opt.anonymize
   log.STATUS_DIR = opt.status_dir
 
   try:
@@ -1232,11 +1222,11 @@
         self_signals[m.mac] = bss_signal
         peer_data[m.mac] = seen_peers
         log.Log('%s: APs=%-4d peer-APs=%s stations=%s',
-                m.AnonymizeMAC(p.me.mac), len(p.seen_bss),
-                ','.join('%s(%d)' % (m.AnonymizeMAC(i.mac), i.rssi)
+                helpers.DecodeMAC(p.me.mac), len(p.seen_bss),
+                ','.join('%s(%d)' % (helpers.DecodeMAC(i.mac), i.rssi)
                          for i in sorted(seen_bss_peers,
                                          key=lambda i: -i.rssi)),
-                ','.join('%s(%d)' % (m.AnonymizeMAC(i.mac), i.rssi)
+                ','.join('%s(%d)' % (helpers.DecodeMAC(i.mac), i.rssi)
                          for i in sorted(p.assoc,
                                          key=lambda i: -i.rssi)))
 
@@ -1251,7 +1241,7 @@
       can2G_count = can5G_count = 0
       for m in managers:
         for assoc in m.assoc_list.itervalues():
-          anon = m.AnonymizeMAC(assoc.mac)
+          station = helpers.DecodeMAC(assoc.mac)
           if log_sta_band_capabilities:
             if assoc.can5G:
               can5G_count += 1
@@ -1259,11 +1249,10 @@
             else:
               can2G_count += 1
               capability = '2.4'
-            log.Log('Connected station %s supports %s GHz', anon, capability)
-          station = helpers.DecodeMAC(assoc.mac)
+            log.Log('Connected station %s supports %s GHz', station, capability)
           species = clientinfo.taxonomize(station)
           if species:
-            log.Log('Connected station %s taxonomy: %s' % (anon, species))
+            log.Log('Connected station %s taxonomy: %s', station, species)
       if log_sta_band_capabilities:
         log.Log('Connected stations: total %d, 5 GHz %d, 2.4 GHz %d',
                 can5G_count + can2G_count, can5G_count, can2G_count)
diff --git a/waveguide/wifiblaster_controller_test.py b/waveguide/wifiblaster_controller_test.py
index 9e300f2..12ff480 100755
--- a/waveguide/wifiblaster_controller_test.py
+++ b/waveguide/wifiblaster_controller_test.py
@@ -71,13 +71,6 @@
     stdout = ('version=1 mac=11:11:11:11:11:11 throughput=10000000 '
               'samples=5000000,15000000\n'
               'malformed 11:11:11:11:11:11 but has macs 11:11:11:11:11:11\n')
-
-    result = wc._AnonymizeResult(stdout)
-    expected = ('version=1 mac=CYAFVU throughput=10000000 '
-                'samples=5000000,15000000\n'
-                'malformed CYAFVU but has macs CYAFVU\n')
-    wvtest.WVPASSEQ(result, expected)
-
     expected = [('version=1 mac=11:11:11:11:11:11 throughput=10000000 '
                  'samples=5000000,15000000'),
                 'malformed 11:11:11:11:11:11 but has macs 11:11:11:11:11:11']
diff --git a/wifi/autochannel.py b/wifi/autochannel.py
index 51b4d00..c669c9a 100644
--- a/wifi/autochannel.py
+++ b/wifi/autochannel.py
@@ -65,6 +65,12 @@
                      % (band, autotype, width))
 
 
+def get_all_frequencies(band):
+  """Get all 802.11 frequencies for the given band."""
+  return get_permitted_frequencies(band, 'OVERLAP' if band == '2.4' else 'ANY',
+                                   '20').split()
+
+
 def scan(interface, band, autotype, width):
   """Do an autochannel scan and return the recommended channel.
 
diff --git a/wifi/autochannel_test.py b/wifi/autochannel_test.py
index a53725f..7198cb5 100755
--- a/wifi/autochannel_test.py
+++ b/wifi/autochannel_test.py
@@ -22,5 +22,17 @@
     wvtest.WVEXCEPT(ValueError, autochannel.get_permitted_frequencies, *case)
 
 
+@wvtest.wvtest
+def get_all_frequencies_test():
+  wvtest.WVPASSEQ(['2412', '2417', '2422', '2427', '2432', '2437', '2442',
+                   '2447', '2452', '2457', '2462'],
+                  autochannel.get_all_frequencies('2.4'))
+
+  wvtest.WVPASSEQ(['5180', '5200', '5220', '5240', '5745', '5765', '5785',
+                   '5805', '5825', '5260', '5280', '5300', '5320', '5500',
+                   '5520', '5540', '5560', '5580', '5660', '5680', '5700'],
+                  autochannel.get_all_frequencies('5'))
+
+
 if __name__ == '__main__':
   wvtest.wvtest_main()
diff --git a/wifi/configs.py b/wifi/configs.py
index 3377a08..97a27ce 100644
--- a/wifi/configs.py
+++ b/wifi/configs.py
@@ -6,6 +6,8 @@
 
 import Crypto.Protocol.KDF
 
+# pylint: disable=g-bad-import-order
+import autochannel
 import experiment
 import utils
 
@@ -373,10 +375,13 @@
                                utils.validate_and_sanitize_bssid(opt.bssid))
   network_block = make_network_block(network_block_lines)
 
+  freq_list = ' '.join(autochannel.get_all_frequencies(opt.band))
+
   lines = [
       'ctrl_interface=/var/run/wpa_supplicant',
       'ap_scan=1',
       'autoscan=exponential:1:30',
+      'freq_list=' + freq_list,
       network_block
   ]
   return '\n'.join(lines)
diff --git a/wifi/configs_test.py b/wifi/configs_test.py
index ab5d6c7..016fc27 100755
--- a/wifi/configs_test.py
+++ b/wifi/configs_test.py
@@ -10,27 +10,36 @@
 from wvtest import wvtest
 
 
+_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'),
+}
+
+
 _WPA_SUPPLICANT_CONFIG = """ctrl_interface=/var/run/wpa_supplicant
 ap_scan=1
 autoscan=exponential:1:30
-network={
+freq_list={freq_list}
+network={{
 \tssid="some ssid"
 \t#psk="some passphrase"
 \tpsk=41821f7ca3ea5d85beea7644ed7e0fefebd654177fa06c26fbdfdc3c599a317f
 \tscan_ssid=1
-}
+}}
 """
 
 _WPA_SUPPLICANT_CONFIG_BSSID = """ctrl_interface=/var/run/wpa_supplicant
 ap_scan=1
 autoscan=exponential:1:30
-network={
+freq_list={freq_list}
+network={{
 \tssid="some ssid"
 \t#psk="some passphrase"
 \tpsk=41821f7ca3ea5d85beea7644ed7e0fefebd654177fa06c26fbdfdc3c599a317f
 \tscan_ssid=1
 \tbssid=12:34:56:78:90:ab
-}
+}}
 """
 
 # pylint: disable=g-backslash-continuation
@@ -38,12 +47,13 @@
 """ctrl_interface=/var/run/wpa_supplicant
 ap_scan=1
 autoscan=exponential:1:30
-network={
+freq_list={freq_list}
+network={{
 \tssid="some ssid"
 \tkey_mgmt=NONE
 \tscan_ssid=1
 \tbssid=12:34:56:78:90:ab
-}
+}}
 """
 
 
@@ -54,24 +64,30 @@
         "Can't test generate_wpa_supplicant_config without wpa_passphrase.")
     return
 
-  opt = FakeOptDict()
-  config = configs.generate_wpa_supplicant_config(
-      'some ssid', 'some passphrase', opt)
-  wvtest.WVPASSEQ(_WPA_SUPPLICANT_CONFIG, config)
+  for band in ('2.4', '5'):
+    opt = FakeOptDict()
+    opt.band = band
+    got = configs.generate_wpa_supplicant_config(
+        'some ssid', 'some passphrase', opt)
+    want = _WPA_SUPPLICANT_CONFIG.format(freq_list=_FREQ_LIST[band])
+    wvtest.WVPASSEQ(want, got)
 
-  opt.bssid = 'TotallyNotValid'
-  wvtest.WVEXCEPT(utils.BinWifiException,
-                  configs.generate_wpa_supplicant_config,
-                  'some ssid', 'some passphrase', opt)
+    opt.bssid = 'TotallyNotValid'
+    wvtest.WVEXCEPT(utils.BinWifiException,
+                    configs.generate_wpa_supplicant_config,
+                    'some ssid', 'some passphrase', opt)
 
-  opt.bssid = '12:34:56:78:90:Ab'
-  config = configs.generate_wpa_supplicant_config(
-      'some ssid', 'some passphrase', opt)
-  wvtest.WVPASSEQ(_WPA_SUPPLICANT_CONFIG_BSSID, config)
+    opt.bssid = '12:34:56:78:90:Ab'
+    got = configs.generate_wpa_supplicant_config(
+        'some ssid', 'some passphrase', opt)
+    want = _WPA_SUPPLICANT_CONFIG_BSSID.format(freq_list=_FREQ_LIST[band])
+    wvtest.WVPASSEQ(want, got)
 
-  config = configs.generate_wpa_supplicant_config(
-      'some ssid', None, opt)
-  wvtest.WVPASSEQ(_WPA_SUPPLICANT_CONFIG_BSSID_UNSECURED, config)
+    got = configs.generate_wpa_supplicant_config(
+        'some ssid', None, opt)
+    want = _WPA_SUPPLICANT_CONFIG_BSSID_UNSECURED.format(
+        freq_list=_FREQ_LIST[band])
+    wvtest.WVPASSEQ(want, got)
 
 
 _PHY_INFO = """Wiphy phy0
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/quantenna.py b/wifi/quantenna.py
index 39dfabf..1408574 100755
--- a/wifi/quantenna.py
+++ b/wifi/quantenna.py
@@ -50,8 +50,8 @@
   return None, None, None, None
 
 
-def _set_link_state(hif, state):
-  subprocess.check_output(['ip', 'link', 'set', 'dev', hif, state])
+def _ifplugd_action(hif, state):
+  subprocess.check_output(['/etc/ifplugd/ifplugd.action', hif, state])
 
 
 def _parse_scan_result(line):
@@ -145,7 +145,7 @@
     _qcsapi('vlan_config', 'pcie0', 'trunk', vlan)
 
     _qcsapi('block_bss', lif, 0)
-    _set_link_state(hif, 'up')
+    _ifplugd_action(hif, 'up')
   except:
     stop_ap_wifi(opt)
     raise
@@ -188,7 +188,7 @@
     _qcsapi('vlan_config', 'pcie0', 'enable')
     _qcsapi('vlan_config', 'pcie0', 'trunk', vlan)
 
-    _set_link_state(hif, 'up')
+    _ifplugd_action(hif, 'up')
   except:
     stop_client_wifi(opt)
     raise
@@ -207,7 +207,7 @@
   except subprocess.CalledProcessError:
     pass
 
-  _set_link_state(hif, 'down')
+  _ifplugd_action(hif, 'down')
 
   return True
 
@@ -223,7 +223,7 @@
   except subprocess.CalledProcessError:
     pass
 
-  _set_link_state(hif, 'down')
+  _ifplugd_action(hif, 'down')
 
   return True
 
diff --git a/wifi/wifi.py b/wifi/wifi.py
index b0ef7f9..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
 
@@ -542,9 +549,20 @@
       ('hostapd_cli', '-i', interface, 'status'), no_stdout=True) == 0
 
 
-def _is_wpa_supplicant_running(interface):
+def _wpa_cli(program, interface, command):
   return utils.subprocess_quiet(
-      ('wpa_cli', '-i', interface, 'status'), no_stdout=True) == 0
+      (program, '-i', interface, command), no_stdout=True) == 0
+
+
+def _is_wpa_supplicant_running(interface):
+  return _wpa_cli('wpa_cli', interface, 'status')
+
+
+def _reconfigure_wpa_supplicant(interface):
+  if not _wpa_cli('wpa_cli', interface, 'reconfigure'):
+    return False
+
+  return _wait_for_wpa_supplicant_to_associate(interface)
 
 
 def _hostapd_debug_options():
@@ -653,6 +671,38 @@
         return None
 
 
+def _wait_for_wpa_supplicant_to_associate(interface):
+  """Wait for wpa_supplicant to associate.
+
+  If it does not associate within a certain period of time, terminate it.
+
+  Args:
+    interface: The interface on which wpa_supplicant is running.
+
+  Raises:
+    BinWifiException: if wpa_supplicant fails to associate and
+    also cannot be stopped to cleanup after the failure.
+
+  Returns:
+    Whether wpa_supplicant associated within the timeout.
+  """
+  utils.log('Waiting for wpa_supplicant to connect')
+  for _ in xrange(100):
+    if _get_wpa_state(interface) == 'COMPLETED':
+      utils.log('ok')
+      return True
+    sys.stderr.write('.')
+    time.sleep(0.1)
+
+  utils.log('wpa_supplicant did not connect.')
+  if not _stop_wpa_supplicant(interface):
+    raise utils.BinWifiException(
+        "Couldn't stop wpa_supplicant after it failed to connect.  "
+        "Consider killing it manually.")
+
+  return False
+
+
 def _start_wpa_supplicant(interface, config_filename):
   """Starts a babysat wpa_supplicant.
 
@@ -704,21 +754,7 @@
   else:
     return False
 
-  utils.log('Waiting for wpa_supplicant to connect')
-  for _ in xrange(100):
-    if _get_wpa_state(interface) == 'COMPLETED':
-      utils.log('ok')
-      return True
-    sys.stderr.write('.')
-    time.sleep(0.1)
-
-  utils.log('wpa_supplicant did not connect.')
-  if not _stop_wpa_supplicant(interface):
-    raise utils.BinWifiException(
-        "Couldn't stop wpa_supplicant after it failed to connect.  "
-        "Consider killing it manually.")
-
-  return False
+  return _wait_for_wpa_supplicant_to_associate(interface)
 
 
 def _maybe_restart_hostapd(interface, config, opt):
@@ -777,8 +813,7 @@
 def _restart_hostapd(band):
   """Restart hostapd from previous options.
 
-  Only used by _maybe_restart_wpa_supplicant, to restart hostapd after stopping
-  it.
+  Only used by _set_wpa_supplicant_config, to restart hostapd after stopping it.
 
   Args:
     band: The band on which to restart hostapd.
@@ -797,7 +832,7 @@
   _run(argv)
 
 
-def _maybe_restart_wpa_supplicant(interface, config, opt):
+def _set_wpa_supplicant_config(interface, config, opt):
   """Starts or restarts wpa_supplicant unless doing so would be a no-op.
 
   The no-op case (i.e. wpa_supplicant is already running with an equivalent
@@ -826,11 +861,12 @@
   except IOError:
     pass
 
-  if not _is_wpa_supplicant_running(interface):
+  already_running = _is_wpa_supplicant_running(interface)
+  if not already_running:
     utils.log('wpa_supplicant not running yet, starting.')
   elif current_config != config:
     # TODO(rofrankel): Consider using wpa_cli reconfigure here.
-    utils.log('wpa_supplicant config changed, restarting.')
+    utils.log('wpa_supplicant config changed, reconfiguring.')
   elif opt.force_restart:
     utils.log('Forced restart requested.')
     forced = True
@@ -838,12 +874,12 @@
     utils.log('wpa_supplicant-%s already configured and running', interface)
     return True
 
-  if not _stop_wpa_supplicant(interface):
-    raise utils.BinWifiException("Couldn't stop wpa_supplicant")
-
   if not forced:
     utils.atomic_write(tmp_config_filename, config)
 
+  # TODO(rofrankel): Consider removing all the restart hostapd stuff when
+  # b/30140131 is resolved.  hostapd seems to keep working without being
+  # restarted, at least on Camaro.
   restart_hostapd = False
   ap_interface = iw.find_interface_from_band(band, iw.INTERFACE_TYPE.ap,
                                              opt.interface_suffix)
@@ -852,13 +888,15 @@
     opt_without_persist = options.OptDict({})
     opt_without_persist.persist = False
     opt_without_persist.band = opt.band
-    # Code review: Will AP and client always have the same suffix?
     opt_without_persist.interface_suffix = opt.interface_suffix
     if not stop_ap_wifi(opt_without_persist):
       raise utils.BinWifiException(
           "Couldn't stop hostapd to start wpa_supplicant.")
 
-  if not _start_wpa_supplicant(interface, tmp_config_filename):
+  if already_running:
+    if not _reconfigure_wpa_supplicant(interface):
+      raise utils.BinWifiException('Failed to reconfigure wpa_supplicant.')
+  elif not _start_wpa_supplicant(interface, tmp_config_filename):
     raise utils.BinWifiException(
         'wpa_supplicant failed to start.  Look at wpa_supplicant logs for '
         'details.')
@@ -934,7 +972,7 @@
           ('ip', 'link', 'set', interface, 'address', mac_address))
 
   wpa_config = configs.generate_wpa_supplicant_config(opt.ssid, psk, opt)
-  if not _maybe_restart_wpa_supplicant(interface, wpa_config, opt):
+  if not _set_wpa_supplicant_config(interface, wpa_config, opt):
     return False
 
   return True
@@ -958,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
 
@@ -1013,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,