Merge "conman:  Enable/disable ACS autoprovisioning."
diff --git a/cmds/dhcp-rogue.c b/cmds/dhcp-rogue.c
index 6fc16c3..8d590bf 100644
--- a/cmds/dhcp-rogue.c
+++ b/cmds/dhcp-rogue.c
@@ -22,8 +22,13 @@
 #include <arpa/inet.h>
 #include <fcntl.h>
 #include <getopt.h>
+#include <net/ethernet.h>
 #include <net/if.h>
 #include <net/if_arp.h>
+#include <net/if_packet.h>
+#include <netinet/ip.h>
+#include <netinet/udp.h>
+#include <netpacket/packet.h>
 #include <stdint.h>
 #include <string.h>
 #include <stdio.h>
@@ -31,9 +36,15 @@
 #include <sys/ioctl.h>
 #include <sys/types.h>
 #include <sys/stat.h>
+#include <time.h>
 #include <unistd.h>
 
+
+#define DHCP_SERVER_PORT  67
+#define DHCP_CLIENT_PORT  68
+
 struct dhcp_message {
+  /* DHCP packet */
   uint8_t op;
   #define OP_BOOTREQUEST 1
   uint8_t htype;
@@ -53,20 +64,32 @@
   char file[128];
   uint8_t magic[4];
   uint8_t type[3];
-};
+  uint8_t end;
+} __attribute__ ((__packed__));
 
-int create_socket(const char *ifname)
+
+struct dhcp_packet {
+  struct ether_header eth;
+  struct ip ip;
+  struct udphdr udp;
+  struct dhcp_message dhcp;
+} __attribute__ ((__packed__));
+
+
+struct udp_checksum_helper {
+  uint32_t ip_src;
+  uint32_t ip_dst;
+  uint8_t rsvd;
+  uint8_t ip_p;
+  uint16_t udp_len;
+  struct udphdr udp;
+  struct dhcp_message dhcp;
+} __attribute__ ((__packed__));
+
+
+void bind_socket_to_device(int s, const char *ifname)
 {
-  int s = socket(AF_INET, SOCK_DGRAM, 0);
-  struct sockaddr_in sin;
   struct ifreq ifr;
-  int enable = 1;
-  struct timeval tv;
-
-  if (s < 0) {
-    perror("socket(AF_INET)");
-    exit(1);
-  }
 
   memset(&ifr, 0, sizeof(ifr));
   snprintf(ifr.ifr_name, sizeof(ifr.ifr_name), "%s", ifname);
@@ -76,6 +99,23 @@
     perror("SO_BINDTODEVICE");
     exit(1);
   }
+}
+
+
+int create_udp_socket(const char *ifname)
+{
+  int s;
+  struct sockaddr_in sin;
+  int enable = 1;
+  int ipttl = 2;
+  struct timeval tv;
+
+  if ((s = socket(AF_INET, SOCK_DGRAM, 0)) < 0) {
+    perror("socket(SOCK_DGRAM)");
+    exit(1);
+  }
+
+  bind_socket_to_device(s, ifname);
 
   if (setsockopt(s, SOL_SOCKET, SO_BROADCAST, &enable, sizeof(enable))) {
     perror("SO_BROADCAST");
@@ -87,24 +127,22 @@
     exit(1);
   }
 
-  tv.tv_sec = 5;
+  tv.tv_sec = 15;
   tv.tv_usec = 0;
   if (setsockopt(s, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv))) {
     perror("SO_RCVTIMEO");
     exit(1);
   }
 
-  memset(&ifr, 0, sizeof(ifr));
-  snprintf(ifr.ifr_name, sizeof(ifr.ifr_name), "%s", ifname);
-
-  if (ioctl(s, SIOCGIFADDR, &ifr) < 0) {
-    perror("SIOCGIFADDR");
+  if (setsockopt(s, IPPROTO_IP, IP_TTL, &ipttl, sizeof(ipttl))) {
+    perror("IP_TTL");
     exit(1);
   }
 
   memset(&sin, 0, sizeof(sin));
   sin.sin_family = AF_INET;
-  sin.sin_port = htons(68);
+  sin.sin_addr.s_addr=htonl(INADDR_ANY);
+  sin.sin_port = htons(DHCP_CLIENT_PORT);
 
   if (bind(s, (struct sockaddr *)&sin, sizeof(sin)) < 0) {
     perror("bind");
@@ -114,6 +152,28 @@
   return s;
 }
 
+
+int create_raw_socket(const char *ifname)
+{
+  int s;
+  int enable = 1;
+
+  if ((s = socket(PF_PACKET, SOCK_RAW, htons(ETH_P_IP))) < 0) {
+    perror("socket(PF_PACKET)");
+    exit(1);
+  }
+
+  bind_socket_to_device(s, ifname);
+
+  if (setsockopt(s, SOL_SOCKET, SO_BROADCAST, &enable, sizeof(enable))) {
+    perror("SO_BROADCAST");
+    exit(1);
+  }
+
+  return s;
+}
+
+
 void get_chaddr(uint8_t *chaddr, int s, const char *ifname)
 {
   struct ifreq ifr;
@@ -134,71 +194,245 @@
   memcpy(chaddr, ifr.ifr_hwaddr.sa_data, 6);
 }
 
-void send_dhcp_discover(int s, const char *ifname)
+uint16_t ipsum(const uint8_t *data, int len)
 {
-  struct dhcp_message msg;
-  struct sockaddr_in sin;
-  socklen_t slen = sizeof(sin);
+  uint32_t sum = 0;
+  const uint16_t *p = (const uint16_t *)data;
 
-  memset(&msg, 0, sizeof(msg));
-  msg.op = OP_BOOTREQUEST;
-  msg.htype = HTYPE_ETHERNET;
-  msg.hlen = 6;
-  msg.xid = 0;
-  msg.flags = htons(FLAGS_BROADCAST);
-  get_chaddr(msg.chaddr, s, ifname);
-  snprintf(msg.sname, sizeof(msg.sname), "%s", "rogue_dhcp_server_detection");
-  msg.magic[0] = 99;  /* DHCP magic number, RFC 2133 */
-  msg.magic[1] = 130;
-  msg.magic[2] = 83;
-  msg.magic[3] = 99;
-  msg.type[0] = 53;  /* option 53, DHCP type. */
-  msg.type[1] = 1;  /* length = 1 */
-  msg.type[2] = 1;  /* DHCPDISCOVER */
+  while (len > 1) {
+    sum += *p++;
+    len -= 2;
+  }
+
+  if (len) {
+    const uint8_t *p8 = (const uint8_t *)p;
+    sum += (uint16_t) *p8;
+  }
+
+  while (sum >> 16) {
+    sum = (sum & 0xFFFF) + (sum >> 16);
+  }
+
+  return ~sum;
+}
+
+
+int getifindex(int s, const char *ifname)
+{
+  struct ifreq ifr;
+
+  memset(&ifr, 0, sizeof(ifr));
+  snprintf(ifr.ifr_name, sizeof(ifr.ifr_name), "%s", ifname);
+
+  if (ioctl(s, SIOCGIFINDEX, &ifr) < 0) {
+    char errbuf[128];
+    snprintf(errbuf, sizeof(errbuf), "SIOCGIFINDEX %s", ifname);
+    perror(errbuf);
+    exit(1);
+  }
+
+  return ifr.ifr_ifindex;
+}
+
+
+void insert_udp_checksum(struct dhcp_packet *pkt)
+{
+  struct udp_checksum_helper csum_helper;
+
+  memset(&csum_helper, 0, sizeof(csum_helper));
+  memcpy(&csum_helper.udp, &pkt->udp, sizeof(csum_helper.udp));
+  memcpy(&csum_helper.dhcp, &pkt->dhcp, sizeof(csum_helper.dhcp));
+  csum_helper.ip_src = pkt->ip.ip_src.s_addr;
+  csum_helper.ip_dst = pkt->ip.ip_dst.s_addr;
+  csum_helper.ip_p = pkt->ip.ip_p;
+  csum_helper.udp_len = pkt->udp.len;
+  pkt->udp.check = ipsum((const uint8_t *)&csum_helper, sizeof(csum_helper));
+}
+
+
+void send_dhcp_discover(int udp_sock, const char *ifname)
+{
+  int s = create_raw_socket(ifname);
+  struct dhcp_packet pkt;
+  struct sockaddr_ll sll;
+  socklen_t slen = sizeof(sll);
+  struct sockaddr_in sin;
+
+  memset(&pkt, 0, sizeof(pkt));
+  memset(&pkt.eth.ether_dhost, 0xff, sizeof(pkt.eth.ether_dhost));
+  get_chaddr(pkt.eth.ether_shost, s, ifname);
+  pkt.eth.ether_type = htons(ETH_P_IP);
+
+  pkt.ip.ip_v = 4;
+  pkt.ip.ip_hl = 5;
+  pkt.ip.ip_ttl = 2;
+  pkt.ip.ip_p = 17;
+  inet_pton(AF_INET, "0.0.0.0", &pkt.ip.ip_src);
+  inet_pton(AF_INET, "255.255.255.255", &pkt.ip.ip_dst);
+  pkt.ip.ip_len = htons(sizeof(pkt.ip) + sizeof(pkt.udp) + sizeof(pkt.dhcp));
+  pkt.ip.ip_sum = ipsum((const uint8_t *)&pkt.ip, sizeof(pkt.ip));
+
+  pkt.udp.source = htons(DHCP_CLIENT_PORT);
+  pkt.udp.dest = htons(DHCP_SERVER_PORT);
+  pkt.udp.len = htons(sizeof(pkt.udp) + sizeof(pkt.dhcp));
+  pkt.udp.check = htons(0);
+
+  pkt.dhcp.op = OP_BOOTREQUEST;
+  pkt.dhcp.htype = HTYPE_ETHERNET;
+  pkt.dhcp.hlen = 6;
+  pkt.dhcp.xid = htonl(time(NULL));
+  pkt.dhcp.secs = htons(1);
+  pkt.dhcp.flags = htons(FLAGS_BROADCAST);
+  get_chaddr(pkt.dhcp.chaddr, s, ifname);
+  snprintf(pkt.dhcp.sname, sizeof(pkt.dhcp.sname), "%s",
+      "rogue_dhcp_server_detection");
+  pkt.dhcp.magic[0] = 99;  /* DHCP magic number, RFC 2133 */
+  pkt.dhcp.magic[1] = 130;
+  pkt.dhcp.magic[2] = 83;
+  pkt.dhcp.magic[3] = 99;
+  pkt.dhcp.type[0] = 53;  /* option 53, DHCP type. */
+  pkt.dhcp.type[1] = 1;  /* length = 1 */
+  pkt.dhcp.type[2] = 1;  /* DHCPDISCOVER */
+  pkt.dhcp.end = 0xff;  /* End option */
+
+  insert_udp_checksum(&pkt);
+
+  memset(&sll, 0, sizeof(sll));
+  sll.sll_family = AF_PACKET;
+  memset(&sll.sll_addr, 0xff, ETH_ALEN);
+  sll.sll_halen = ETH_ALEN;
+  sll.sll_ifindex = getifindex(s, ifname);
+  sll.sll_pkttype = PACKET_BROADCAST;
+
+  if (sendto(s, &pkt, sizeof(pkt), 0,
+             (const struct sockaddr *)&sll, slen) < 0) {
+    perror("sendto");
+    exit(1);
+  }
+
+  close(s);
+
+  /*
+   * We send two DHCP requests. The PF_PACKET socket above
+   * sends a packet with a soruce IP address of 0.0.0.0, and
+   * sends it straight to the Ethernet link such that the
+   * local dnsmasq does not see it.
+   *
+   * We send another one here using a PF_INET socket, which
+   * will have a source IP address of this node, and which will
+   * also be copied to the local dnsmasq.
+   */
 
   memset(&sin, 0, sizeof(sin));
   sin.sin_family = AF_INET;
   sin.sin_addr.s_addr = htonl(INADDR_BROADCAST);
-  sin.sin_port = htons(67);
+  sin.sin_port = htons(DHCP_SERVER_PORT);
+  slen = sizeof(sin);
 
-  if (sendto(s, &msg, sizeof(msg), 0,
+  if (sendto(udp_sock, &pkt.dhcp, sizeof(pkt.dhcp), 0,
              (const struct sockaddr *)&sin, slen) < 0) {
-    perror("sendto enable");
+    perror("sendto");
     exit(1);
   }
 }
 
+
+static int cmp_in_addr_p(const void *p1, const void *p2)
+{
+  const struct in_addr *i1 = (const struct in_addr *)p1;
+  const struct in_addr *i2 = (const struct in_addr *)p2;
+
+  if (i1->s_addr == i2->s_addr) {
+    return 0;
+  } else if (i1->s_addr < i2->s_addr) {
+    return -1;
+  } else {
+    return 1;
+  }
+}
+
+
 void receive_dhcp_offers(int s)
 {
   struct sockaddr_in sin;
   socklen_t slen = sizeof(sin);
-  uint8_t buf[2048];
+  uint8_t pktbuf[2048];
+  #define MAX_RESPONSES 4
+  struct in_addr responses[MAX_RESPONSES];
+  int nresponses = 0;
 
   memset(&sin, 0, sizeof(sin));
-  while (recvfrom(s, buf, sizeof(buf), 0, &sin, &slen) > 0) {
-    char ipbuf[64];
-    inet_ntop(AF_INET, &sin.sin_addr, ipbuf, sizeof(ipbuf));
-    printf("DHCP response from %s\n", ipbuf);
+  memset(responses, 0, sizeof(responses));
+
+  while (recvfrom(s, pktbuf, sizeof(pktbuf), 0, &sin, &slen) > 0) {
+    int duplicate = 0;
+    int i;
+
+    if (nresponses >= MAX_RESPONSES) {
+      break;
+    }
+
+    for (i = 0; i < MAX_RESPONSES; ++i) {
+      if (responses[i].s_addr == sin.sin_addr.s_addr) {
+        duplicate = 1;
+        break;
+      }
+    }
+
+    if (!duplicate) {
+      responses[nresponses].s_addr = sin.sin_addr.s_addr;
+      nresponses++;
+    }
+  }
+
+  if (nresponses == 0) {
+    printf("Received 0 DHCP responses.\n");
+  } else {
+    char outbuf[(MAX_RESPONSES * (INET_ADDRSTRLEN + 1)) + 1];
+    int i;
+
+    qsort(responses, nresponses, sizeof(responses[0]), cmp_in_addr_p);
+
+    outbuf[0] = '\0';
+    for (i = 0; i < nresponses; ++i) {
+      int len = strlen(outbuf);
+      int lim = sizeof(outbuf) - len;
+
+      if (i > 0) {
+        strcat(outbuf, ",");
+        len = strlen(outbuf);
+        lim = sizeof(outbuf) - len;
+      }
+
+      inet_ntop(AF_INET, &responses[i], outbuf + len, lim);
+    }
+
+    /*
+     * Yes, this will print "Received 1 DHCP responses". It
+     * complicates any matching code to make the 's' optional,
+     * for no benefit. OCD will have to find a way to cope.
+     */
+    printf("Received %d DHCP responses from: %s\n", nresponses, outbuf);
   }
 }
 
 void usage(const char *progname)
 {
-  printf("usage: %s [-i br0]\n", progname);
-  printf("\t-i: name of the interface to probe for rogue DHCP servers.\n");
+  fprintf(stderr, "usage: %s [-i br0] [-l]\n", progname);
+  fprintf(stderr, "\t-i: name of the interface to probe for DHCP servers.\n");
+  fprintf(stderr, "\t-l: show a response from localhost\n");
   exit(1);
 }
 
 int main(int argc, char **argv)
 {
-  int s;
   const char *interface = "br0";
   struct option long_options[] = {
     {"interface", required_argument, 0, 'i'},
-    {0,          0,                 0, 0},
+    {0,           0,                 0, 0},
   };
+  int s, c;
 
-  int c;
   while ((c = getopt_long(argc, argv, "i:", long_options, NULL)) != -1) {
     switch (c) {
     case 'i':
@@ -211,7 +445,8 @@
     }
   }
 
-  s = create_socket(interface);
+  setlinebuf(stdout);
+  s = create_udp_socket(interface);
   send_dhcp_discover(s, interface);
   receive_dhcp_offers(s);
 }
diff --git a/conman/connection_manager.py b/conman/connection_manager.py
index 5faa789..708954f 100755
--- a/conman/connection_manager.py
+++ b/conman/connection_manager.py
@@ -85,6 +85,10 @@
     if self.ssid is None:
       raise ValueError('Command file does not specify SSID')
 
+    if self.wifi.initial_ssid == self.ssid:
+      logging.debug('Connected to WLAN at startup')
+      self.client_up = True
+
   def start_access_point(self):
     """Start an access point."""
 
@@ -241,11 +245,21 @@
 
     # If the ethernet file doesn't exist for any reason when conman starts,
     # check explicitly and run ifplugd.action to create the file.
-    if not os.path.exists(os.path.join(self._interface_status_dir,
-                                       self.ETHERNET_STATUS_FILE)):
-      ethernet_up = self.is_ethernet_up()
-      self.bridge.ethernet = ethernet_up
+    if not os.path.exists(os.path.join(self._interface_status_dir, 'eth0')):
+      ethernet_up = self.is_interface_up('eth0')
       self.ifplugd_action('eth0', ethernet_up)
+      self.bridge.ethernet = ethernet_up
+
+    # Do the same for wifi interfaces , but rather than explicitly setting that
+    # the wpa_supplicant link is up, attempt to attach to the wpa_supplicant
+    # control interface.
+    for wifi in self.wifi:
+      if not os.path.exists(
+          os.path.join(self._interface_status_dir, wifi.name)):
+        wifi_up = self.is_interface_up(wifi.name)
+        self.ifplugd_action(wifi.name, wifi_up)
+        if wifi_up:
+          wifi.attach_wpa_control(self._wpa_control_interface)
 
     for path, prefix in ((self._status_dir, self.GATEWAY_FILE_PREFIX),
                          (self._interface_status_dir, ''),
@@ -263,13 +277,16 @@
     self._interface_update_counter = 0
     self._try_wlan_after = {'5': 0, '2.4': 0}
 
-  def is_ethernet_up(self):
-    """Explicitly check whether ethernet is up.
+  def is_interface_up(self, interface_name):
+    """Explicitly check whether an interface is up.
 
     Only used on startup, and only if ifplugd file is missing.
 
+    Args:
+      interface_name:  The name of the interface to check.
+
     Returns:
-      Whether the ethernet link is up.
+      Whether the interface is up.
     """
     try:
       lines = subprocess.check_output(self.IP_LINK).splitlines()
@@ -277,7 +294,7 @@
       raise EnvironmentError('Failed to call "ip link": %r', e.message)
 
     for line in lines:
-      if 'eth0' in line and 'LOWER_UP' in line:
+      if interface_name in line and 'LOWER_UP' in line:
         return True
 
     return False
diff --git a/conman/connection_manager_test.py b/conman/connection_manager_test.py
index 0764ece..335f7ee 100755
--- a/conman/connection_manager_test.py
+++ b/conman/connection_manager_test.py
@@ -193,13 +193,39 @@
   WIFI_SETCLIENT = ['echo', 'setclient']
   IFUP = ['echo', 'ifup']
   IFPLUGD_ACTION = ['echo', 'ifplugd.action']
-  # This simulates the output of 'ip link' when eth0 is up.
-  IP_LINK = ['echo', 'eth0 LOWER_UP']
 
   def __init__(self, *args, **kwargs):
+    self.interfaces_already_up = kwargs.pop('__test_interfaces_already_up',
+                                            ['eth0'])
+
+    wifi_interfaces_already_up = [ifc for ifc in self.interfaces_already_up
+                                  if ifc.startswith('wcli')]
+    for wifi in wifi_interfaces_already_up:
+      # wcli1 is always 5 GHz.  wcli0 always *includes* 2.4.
+      band = '5' if wifi == 'wcli1' else '2.4'
+      # This will happen in the super function, but in order for
+      # write_wlan_config to work we have to do it now.  This has to happen
+      # before the super function so that the files exist before the inotify
+      # registration.
+      self._config_dir = kwargs['config_dir']
+      self.write_wlan_config(band, 'my ssid', 'passphrase')
+
+      # Also create the wpa_supplicant socket to which to attach.
+      open(os.path.join(kwargs['wpa_control_interface'], wifi), 'w')
+
     super(ConnectionManager, self).__init__(*args, **kwargs)
+
+    for wifi in wifi_interfaces_already_up:
+      # pylint: disable=protected-access
+      self.interface_by_name(wifi)._initially_connected = True
+
     self.scan_has_results = False
 
+  @property
+  def IP_LINK(self):
+    return ['echo'] + ['%s LOWER_UP' % ifc
+                       for ifc in self.interfaces_already_up]
+
   def _update_access_point(self, wlan_configuration):
     client_was_up = wlan_configuration.client_up
     super(ConnectionManager, self)._update_access_point(wlan_configuration)
@@ -356,7 +382,7 @@
       self.run_once()
 
 
-def connection_manager_test(radio_config):
+def connection_manager_test(radio_config, **cm_kwargs):
   """Returns a decorator that does ConnectionManager test boilerplate."""
   def inner(f):
     """The actual decorator."""
@@ -388,7 +414,8 @@
                               wpa_control_interface=wpa_control_interface,
                               run_duration_s=run_duration_s,
                               interface_update_period=interface_update_period,
-                              wifi_scan_period_s=wifi_scan_period_s)
+                              wifi_scan_period_s=wifi_scan_period_s,
+                              **cm_kwargs)
 
         c.test_interface_update_period = interface_update_period
         c.test_wifi_scan_period = wifi_scan_period
@@ -727,6 +754,7 @@
   wvtest.WVPASS(c.wifi_for_band('5').current_route())
 
 
+
 @wvtest.wvtest
 @connection_manager_test(WIFI_SHOW_OUTPUT_ONE_RADIO_NO_5GHZ)
 def connection_manager_test_one_radio_no_5ghz(c):
@@ -764,5 +792,18 @@
   wvtest.WVPASS(c.wifi_for_band('2.4').current_route())
 
 
+@wvtest.wvtest
+@connection_manager_test(WIFI_SHOW_OUTPUT_ONE_RADIO,
+                         __test_interfaces_already_up=['eth0', 'wcli0'])
+def connection_manager_test_wifi_already_up(c):
+  """Test ConnectionManager when wifi is already up.
+
+  Args:
+    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)
+
+
 if __name__ == '__main__':
   wvtest.wvtest_main()
diff --git a/conman/interface.py b/conman/interface.py
index 13cadc5..0f42e20 100755
--- a/conman/interface.py
+++ b/conman/interface.py
@@ -319,6 +319,7 @@
     self.bands = kwargs.pop('bands', [])
     super(Wifi, self).__init__(*args, **kwargs)
     self._wpa_control = None
+    self.initial_ssid = None
 
   @property
   def wpa_supplicant(self):
@@ -344,8 +345,14 @@
         logging.error('Error attaching to wpa_supplicant: %s', e)
         return
 
-      self.wpa_supplicant = ('wpa_state=COMPLETED' in
-                             self._wpa_control.request('STATUS'))
+      for line in self._wpa_control.request('STATUS').splitlines():
+        if '=' not in line:
+          continue
+        key, value = line.split('=', 1)
+        if key == 'wpa_state':
+          self.wpa_supplicant = value == 'COMPLETED'
+        elif key == 'ssid' and not self._initialized:
+          self.initial_ssid = value
 
   def get_wpa_control(self, socket):
     return wpactrl.WPACtrl(socket)
@@ -380,3 +387,10 @@
             break
 
         self.update_routes()
+
+  def initialize(self):
+    """Unset self.initial_ssid, which is only relevant during initialization."""
+
+    self.initial_ssid = None
+    super(Wifi, self).initialize()
+
diff --git a/conman/interface_test.py b/conman/interface_test.py
index f15764f..f6e03d2 100755
--- a/conman/interface_test.py
+++ b/conman/interface_test.py
@@ -89,7 +89,8 @@
 
   def request(self, request_type):
     if request_type == 'STATUS':
-      return 'foo\nwpa_state=COMPLETED\nbar' if self.connected else 'foo'
+      return ('foo\nwpa_state=COMPLETED\nssid=my ssid\nbar' if self.connected
+              else 'foo')
     else:
       raise ValueError('Invalid request_type %s' % request_type)
 
@@ -234,11 +235,15 @@
     w.detach_wpa_control()
     # pylint: disable=protected-access
     w._initially_connected = True
+    w._initialized = False
     w.attach_wpa_control(wpa_path)
     wpa_control = w._wpa_control
 
     # wpa_supplicant was already connected when we attached.
     wvtest.WVPASS(w.wpa_supplicant)
+    wvtest.WVPASSEQ(w.initial_ssid, 'my ssid')
+    w.initialize()
+    wvtest.WVPASSEQ(w.initial_ssid, None)
 
     # The wpa_supplicant process disconnects and terminates.
     wpa_control.add_event(Wifi.DISCONNECTED_EVENT)
diff --git a/jsonpoll/jsonpoll.py b/jsonpoll/jsonpoll.py
index 9e78b8e..7e0c7ea 100755
--- a/jsonpoll/jsonpoll.py
+++ b/jsonpoll/jsonpoll.py
@@ -31,7 +31,7 @@
 jsonpoll [options]
 --
 host=            host to connect to [localhost]
-port=            port to connect to [8000]
+port=            port to connect to [8080]
 i,interval=      poll interval in seconds [15]
 """
 
diff --git a/taxonomy/ethernet.py b/taxonomy/ethernet.py
index f8966f3..d0aaf45 100644
--- a/taxonomy/ethernet.py
+++ b/taxonomy/ethernet.py
@@ -61,6 +61,7 @@
     '7c:61:93': ['htc'],
     '84:7a:88': ['htc'],
     '90:e7:c4': ['htc'],
+    'a0:f4:50': ['htc'],
     'b4:ce:f6': ['htc'],
     'd8:b3:77': ['htc'],
     'e8:99:c4': ['htc'],
@@ -110,6 +111,7 @@
     '14:7d:c5': ['murata'],
     '1c:99:4c': ['murata'],
     '20:02:af': ['murata'],
+    '40:f3:08': ['murata'],
     '44:a7:cf': ['murata'],
     '5c:da:d4': ['murata'],
     '78:4b:87': ['murata'],
diff --git a/taxonomy/pcaptest.py b/taxonomy/pcaptest.py
index a44a7b6..c7863fb 100644
--- a/taxonomy/pcaptest.py
+++ b/taxonomy/pcaptest.py
@@ -25,6 +25,9 @@
   ('Unknown', './testdata/pcaps/HTC Evo 2.4GHz.pcap'),
   ('Unknown', './testdata/pcaps/HTC Incredible 2.4GHz.pcap'),
   ('Unknown', './testdata/pcaps/HTC Inspire 2.4GHz.pcap'),
+  ('Unknown', './testdata/pcaps/HTC One V 2.4GHz.pcap'),
+  ('Unknown', './testdata/pcaps/HTC One X 2.4GHz.pcap'),
+  ('Unknown', './testdata/pcaps/HTC One X 5GHz.pcap'),
   ('Unknown', './testdata/pcaps/HTC Sensation 2.4GHz.pcap'),
   ('Unknown', './testdata/pcaps/HTC Thunderbolt 2.4GHz.pcap'),
   ('Unknown', './testdata/pcaps/HTC Titan 2.4GHz.pcap'),
@@ -46,7 +49,6 @@
   ('Unknown', './testdata/pcaps/Samsung Exhibit 2.4GHz.pcap'),
   ('Unknown', './testdata/pcaps/Samsung Fascinate 2.4GHz.pcap'),
   ('Unknown', './testdata/pcaps/Samsung Galaxy Tab 2 2.4GHz.pcap'),
-  ('Unknown', './testdata/pcaps/Samsung Infuse 2.4GHz.pcap'),
   ('Unknown', './testdata/pcaps/Samsung Infuse 5GHz.pcap'),
   ('Unknown', './testdata/pcaps/Samsung Vibrant 2.4GHz.pcap'),
   ('Unknown', './testdata/pcaps/Sony Xperia Z5 2.4GHz.pcap'),
@@ -57,6 +59,10 @@
   # work for these, instead we add them explicitly.
   ('iPad (1st/2nd gen)', './testdata/pcaps/iPad 1st gen 5GHz.pcap'),
   ('iPad (1st/2nd gen)', './testdata/pcaps/iPad 2nd gen 5GHz.pcap'),
+  ('iPad (4th gen or Air)', './testdata/pcaps/iPad (4th gen) 5GHz.pcap'),
+  ('iPad (4th gen or Air)', './testdata/pcaps/iPad (4th gen) 2.4GHz.pcap'),
+  ('iPad (4th gen or Air)', './testdata/pcaps/iPad Air 5GHz.pcap'),
+  ('iPad (4th gen or Air)', './testdata/pcaps/iPad Air 2.4GHz.pcap'),
   ('iPhone 6/6+', './testdata/pcaps/iPhone 6 5GHz.pcap'),
   ('iPhone 6/6+', './testdata/pcaps/iPhone 6+ 5GHz.pcap'),
   ('iPhone 6s/6s+', './testdata/pcaps/iPhone 6s 2.4GHz.pcap'),
@@ -65,6 +71,8 @@
   ('iPhone 6s/6s+', './testdata/pcaps/iPhone 6s+ 5GHz.pcap'),
   ('iPod Touch 1st/2nd gen', './testdata/pcaps/iPod Touch 1st gen 2.4GHz.pcap'),
   ('Nest Thermostat v1/v2', './testdata/pcaps/Nest Thermostat 2.4GHz.pcap'),
+  ('Samsung Galaxy S2 or Infuse', './testdata/pcaps/Samsung Galaxy S2 2.4GHz.pcap'),
+  ('Samsung Galaxy S2 or Infuse', './testdata/pcaps/Samsung Infuse 2.4GHz.pcap'),
 ]
 
 
diff --git a/taxonomy/testdata/dhcp.leases b/taxonomy/testdata/dhcp.leases
index 4bbfb3d..64aa1ca 100644
--- a/taxonomy/testdata/dhcp.leases
+++ b/taxonomy/testdata/dhcp.leases
@@ -45,3 +45,4 @@
 1432237016 04:0c:ce:cf:40:2c 192.168.42.35 MacbookAir2010
 1432237016 8c:2d:aa:9c:ce:0f 192.168.42.36 iPood-5
 1432237016 dc:86:d8:a0:c8:de 192.168.42.37 iPhoone-5c
+1432237016 54:ae:27:32:ef:7f 192.168.42.38 iPaad-Air-1
diff --git a/taxonomy/testdata/dhcp.signatures b/taxonomy/testdata/dhcp.signatures
index 5a1ef1d..4831315 100644
--- a/taxonomy/testdata/dhcp.signatures
+++ b/taxonomy/testdata/dhcp.signatures
@@ -37,3 +37,4 @@
 04:0c:ce:cf:40:2c 1,3,6,15,119,95,252,44,46
 8c:2d:aa:9c:ce:0f 1,3,6,15,119,252
 dc:86:d8:a0:c8:de 1,3,6,15,119,252
+54:ae:27:32:ef:7f 1,3,6,15,119,252
diff --git a/taxonomy/testdata/pcaps/HTC One V 2.4GHz.pcap b/taxonomy/testdata/pcaps/HTC One V 2.4GHz.pcap
new file mode 100644
index 0000000..5efec50
--- /dev/null
+++ b/taxonomy/testdata/pcaps/HTC One V 2.4GHz.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/HTC One X 2.4GHz.pcap b/taxonomy/testdata/pcaps/HTC One X 2.4GHz.pcap
new file mode 100644
index 0000000..e57a6b2
--- /dev/null
+++ b/taxonomy/testdata/pcaps/HTC One X 2.4GHz.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/HTC One X 5GHz.pcap b/taxonomy/testdata/pcaps/HTC One X 5GHz.pcap
new file mode 100644
index 0000000..9a25012
--- /dev/null
+++ b/taxonomy/testdata/pcaps/HTC One X 5GHz.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/Moto X 2.4GHz Broadcast.pcap b/taxonomy/testdata/pcaps/Moto X 2.4GHz Broadcast.pcap
new file mode 100644
index 0000000..045ec57
--- /dev/null
+++ b/taxonomy/testdata/pcaps/Moto X 2.4GHz Broadcast.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/Moto X 2.4GHz Specific.pcap b/taxonomy/testdata/pcaps/Moto X 2.4GHz Specific.pcap
new file mode 100644
index 0000000..0399b6c
--- /dev/null
+++ b/taxonomy/testdata/pcaps/Moto X 2.4GHz Specific.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/Moto X 2.4GHz.pcap b/taxonomy/testdata/pcaps/Moto X 2.4GHz.pcap
new file mode 100644
index 0000000..40e0ca1
--- /dev/null
+++ b/taxonomy/testdata/pcaps/Moto X 2.4GHz.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/Moto X 5GHz.pcap b/taxonomy/testdata/pcaps/Moto X 5GHz.pcap
new file mode 100644
index 0000000..dffcc63
--- /dev/null
+++ b/taxonomy/testdata/pcaps/Moto X 5GHz.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/Samsung Galaxy S2 2.4GHz.pcap b/taxonomy/testdata/pcaps/Samsung Galaxy S2 2.4GHz.pcap
new file mode 100644
index 0000000..e772fa8
--- /dev/null
+++ b/taxonomy/testdata/pcaps/Samsung Galaxy S2 2.4GHz.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/Samsung Galaxy S2 5GHz.pcap b/taxonomy/testdata/pcaps/Samsung Galaxy S2 5GHz.pcap
new file mode 100644
index 0000000..035fc85
--- /dev/null
+++ b/taxonomy/testdata/pcaps/Samsung Galaxy S2 5GHz.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/Samsung Galaxy S4 2.4GHz I9505.pcap b/taxonomy/testdata/pcaps/Samsung Galaxy S4 2.4GHz I9505.pcap
new file mode 100644
index 0000000..83832ba
--- /dev/null
+++ b/taxonomy/testdata/pcaps/Samsung Galaxy S4 2.4GHz I9505.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/Samsung Galaxy S4 5GHz I9505.pcap b/taxonomy/testdata/pcaps/Samsung Galaxy S4 5GHz I9505.pcap
new file mode 100644
index 0000000..ed78fc2
--- /dev/null
+++ b/taxonomy/testdata/pcaps/Samsung Galaxy S4 5GHz I9505.pcap
Binary files differ
diff --git "a/taxonomy/testdata/pcaps/iPad \0504th gen\051 2.4GHz.pcap" "b/taxonomy/testdata/pcaps/iPad \0504th gen\051 2.4GHz.pcap"
new file mode 100644
index 0000000..7eb0924
--- /dev/null
+++ "b/taxonomy/testdata/pcaps/iPad \0504th gen\051 2.4GHz.pcap"
Binary files differ
diff --git a/taxonomy/testdata/pcaps/iPad Air 2.4GHz.pcap b/taxonomy/testdata/pcaps/iPad Air 2.4GHz.pcap
new file mode 100644
index 0000000..6d6fabb
--- /dev/null
+++ b/taxonomy/testdata/pcaps/iPad Air 2.4GHz.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/iPad Air 5GHz.pcap b/taxonomy/testdata/pcaps/iPad Air 5GHz.pcap
new file mode 100644
index 0000000..fac188d
--- /dev/null
+++ b/taxonomy/testdata/pcaps/iPad Air 5GHz.pcap
Binary files differ
diff --git a/taxonomy/wifi.py b/taxonomy/wifi.py
index b7cfaa6..c312c05 100644
--- a/taxonomy/wifi.py
+++ b/taxonomy/wifi.py
@@ -179,21 +179,21 @@
         ('BCM4330', 'iPad (3rd gen)', '2.4GHz'),
 
     'wifi3|probe:0,1,45,127,107,221(001018,2),221(00904c,51),221(0050f2,8),htcap:01fe,htagg:1b,htmcs:0000ffff,intwrk:0f,extcap:00000804|assoc:0,1,33,36,48,45,221(001018,2),221(00904c,51),221(0050f2,2),cap:0011,htcap:01fe,htagg:1b,htmcs:0000ffff,txpow:e708|os:ios':
-        ('BCM4334', 'iPad (4th gen)', '5GHz'),
+        ('BCM4334', 'iPad (4th gen or Air)', '5GHz'),
     'wifi3|probe:0,1,45,127,107,221(001018,2),221(00904c,51),221(0050f2,8),htcap:01fe,htagg:1b,htmcs:0000ffff,intwrk:0f,extcap:00000804|assoc:0,1,33,36,48,45,70,221(001018,2),221(00904c,51),221(0050f2,2),cap:0011,htcap:01fe,htagg:1b,htmcs:0000ffff,txpow:e708|os:ios':
-        ('BCM4334', 'iPad (4th gen)', '5GHz'),
+        ('BCM4334', 'iPad (4th gen or Air)', '5GHz'),
     'wifi3|probe:0,1,45,127,107,221(001018,2),221(00904c,51),221(0050f2,8),htcap:01fe,htagg:1b,htmcs:0000ffff,intwrk:ff,extcap:00000804|assoc:0,1,33,36,48,45,221(001018,2),221(00904c,51),221(0050f2,2),cap:0011,htcap:01fe,htagg:1b,htmcs:0000ffff,txpow:e708|os:ios':
-        ('BCM4334', 'iPad (4th gen)', '5GHz'),
+        ('BCM4334', 'iPad (4th gen or Air)', '5GHz'),
     'wifi3|probe:0,1,45,127,107,221(001018,2),221(00904c,51),221(0050f2,8),htcap:01fe,htagg:1b,htmcs:0000ffff,intwrk:ff,extcap:00000804|assoc:0,1,33,36,48,45,70,221(001018,2),221(00904c,51),221(0050f2,2),cap:0011,htcap:01fe,htagg:1b,htmcs:0000ffff,txpow:e708|os:ios':
-        ('BCM4334', 'iPad (4th gen)', '5GHz'),
+        ('BCM4334', 'iPad (4th gen or Air)', '5GHz'),
     'wifi3|probe:0,1,50,3,45,127,107,221(001018,2),221(00904c,51),221(0050f2,8),htcap:01bc,htagg:1b,htmcs:0000ffff,intwrk:0f,extcap:00000804|assoc:0,1,33,36,48,50,45,221(001018,2),221(00904c,51),221(0050f2,2),cap:0431,htcap:01bc,htagg:1b,htmcs:0000ffff,txpow:1805|os:ios':
-        ('BCM4334', 'iPad (4th gen)', '2.4GHz'),
+        ('BCM4334', 'iPad (4th gen or Air)', '2.4GHz'),
     'wifi3|probe:0,1,50,3,45,127,107,221(001018,2),221(00904c,51),221(0050f2,8),htcap:01bc,htagg:1b,htmcs:0000ffff,intwrk:0f,extcap:00000804|assoc:0,1,33,36,48,50,45,70,221(001018,2),221(00904c,51),221(0050f2,2),cap:0431,htcap:01bc,htagg:1b,htmcs:0000ffff,txpow:1805|os:ios':
-        ('BCM4334', 'iPad (4th gen)', '2.4GHz'),
+        ('BCM4334', 'iPad (4th gen or Air)', '2.4GHz'),
     'wifi3|probe:0,1,50,3,45,127,107,221(001018,2),221(00904c,51),221(0050f2,8),htcap:01bc,htagg:1b,htmcs:0000ffff,intwrk:ff,extcap:00000804|assoc:0,1,33,36,48,50,45,221(001018,2),221(00904c,51),221(0050f2,2),cap:0431,htcap:01bc,htagg:1b,htmcs:0000ffff,txpow:1805|os:ios':
-        ('BCM4334', 'iPad (4th gen)', '2.4GHz'),
+        ('BCM4334', 'iPad (4th gen or Air)', '2.4GHz'),
     'wifi3|probe:0,1,50,3,45,127,107,221(001018,2),221(00904c,51),221(0050f2,8),htcap:01bc,htagg:1b,htmcs:0000ffff,intwrk:ff,extcap:00000804|assoc:0,1,33,36,48,50,45,70,221(001018,2),221(00904c,51),221(0050f2,2),cap:0431,htcap:01bc,htagg:1b,htmcs:0000ffff,txpow:1805|os:ios':
-        ('BCM4334', 'iPad (4th gen)', '2.4GHz'),
+        ('BCM4334', 'iPad (4th gen or Air)', '2.4GHz'),
 
     'wifi|probe:0,1,45,127,107,221(001018,2),221(00904c,51),221(0050f2,8),htcap:01fe|assoc:0,1,33,36,48,45,70,221(001018,2),221(00904c,51),221(0050f2,2),htcap:01fe|os:ios':
         ('BCM43241', 'iPad Air (1st gen)', '5GHz'),
@@ -382,6 +382,8 @@
 
     'wifi3|probe:0,1,45,221(00904c,51),htcap:09ef,htagg:1b,htmcs:0000ffff|assoc:0,1,33,36,48,45,221(00904c,51),221(0050f2,2),cap:0011,htcap:09ef,htagg:1b,htmcs:0000ffff,txpow:0005|os:macos':
         ('BCM4331', 'MacBook Pro 17" late 2011 (A1297)', '5GHz'),
+    'wifi3|probe:0,1,3,45,221(00904c,51),htcap:09ef,htagg:1b,htmcs:0000ffff|assoc:0,1,33,36,48,45,221(00904c,51),221(0050f2,2),cap:0011,htcap:09ef,htagg:1b,htmcs:0000ffff,txpow:0005|os:macos':
+        ('BCM4331', 'MacBook Pro 17" late 2011 (A1297)', '5GHz'),
     'wifi3|probe:0,1,50,3,45,221(00904c,51),htcap:19ad,htagg:1b,htmcs:0000ffff|assoc:0,1,33,36,48,50,45,221(00904c,51),221(0050f2,2),cap:0431,htcap:19ad,htagg:1b,htmcs:0000ffff,txpow:1305|os:macos':
         ('BCM4331', 'MacBook Pro 17" late 2011 (A1297)', '2.4GHz'),
 
@@ -413,9 +415,11 @@
     'wifi3|probe:0,1,50,3,45,221(0050f2,8),htcap:012c,htagg:03,htmcs:000000ff|assoc:0,1,50,48,45,221(0050f2,2),127,cap:8431,htcap:012c,htagg:03,htmcs:000000ff,extcap:00000a02|oui:motorola':
         ('QCA_WCN3620', 'Moto G or Moto X', '2.4GHz'),
 
-    'wifi|probe:0,1,45,221(0050f2,8),191,htcap:016e,vhtcap:31800120|assoc:0,1,33,36,48,45,221(0050f2,2),191,127,htcap:016e,vhtcap:31800120|oui:motorola':
+    'wifi3|probe:0,1,45,221(0050f2,8),191,htcap:016e,htagg:03,htmcs:000000ff,vhtcap:31800120,vhtrxmcs:0000fffe,vhttxmcs:0000fffe|assoc:0,1,48,45,221(0050f2,2),191,127,127,cap:0431,htcap:016e,htagg:03,htmcs:000000ff,vhtcap:31800120,vhtrxmcs:0000fffe,vhttxmcs:0000fffe,extcap:00000a02|oui:motorola':
         ('QCA_WCN3680', 'Moto X', '5GHz'),
-    'wifi|probe:0,1,50,3,45,221(0050f2,8),191,htcap:012c,vhtcap:31800120|assoc:0,1,50,48,45,221(0050f2,2),htcap:012c|oui:motorola':
+    'wifi3|probe:0,1,50,3,45,221(0050f2,8),htcap:012c,htagg:03,htmcs:000000ff|assoc:0,1,50,48,45,221(0050f2,2),127,cap:0431,htcap:012c,htagg:03,htmcs:000000ff,extcap:00000a02|oui:motorola':
+        ('QCA_WCN3680', 'Moto X', '2.4GHz'),
+    'wifi3|probe:0,1,50,3,45,221(0050f2,8),191,htcap:012c,htagg:03,htmcs:000000ff,vhtcap:31800120,vhtrxmcs:0000fffe,vhttxmcs:0000fffe|assoc:0,1,50,48,45,221(0050f2,2),127,cap:0431,htcap:012c,htagg:03,htmcs:000000ff,extcap:00000a02|oui:motorola':
         ('QCA_WCN3680', 'Moto X', '2.4GHz'),
 
     'wifi3|probe:0,1,3,45,221(0050f2,8),191,htcap:016e,htagg:03,htmcs:000000ff,vhtcap:31805120,vhtrxmcs:0000fffe,vhttxmcs:0000fffe|assoc:0,1,48,45,221(0050f2,2),191,127,127,cap:8431,htcap:016e,htagg:03,htmcs:000000ff,vhtcap:31805120,vhtrxmcs:0000fffe,vhttxmcs:0000fffe,extcap:00000a02|oui:motorola':
@@ -658,6 +662,23 @@
     'wifi3|probe:0,1,50,3,45,127,107,221(506f9a,16),221(00904c,4),221(0050f2,8),221(001018,2),htcap:01ad,htagg:17,htmcs:0000ffff,intwrk:0f,extcap:00080f8401400040|assoc:0,1,50,33,36,48,45,221(001018,2),221(0050f2,2),cap:0431,htcap:01ad,htagg:17,htmcs:0000ffff,txpow:1202|oui:murata':
         ('BCM4359', 'Samsung Galaxy Note 5', '2.4GHz'),
 
+    'wifi3|probe:0,1,45,221(001018,2),221(00904c,51),htcap:000c,htagg:19,htmcs:000000ff|assoc:0,1,33,36,48,45,221(001018,2),221(00904c,51),221(0050f2,2),cap:0011,htcap:000c,htagg:19,htmcs:000000ff,txpow:0f0a|oui:samsung':
+        ('', 'Samsung Galaxy S2', '5GHz'),
+    'wifi3|probe:0,1,45,221(001018,2),221(00904c,51),htcap:000c,htagg:19,htmcs:000000ff|assoc:0,1,33,36,48,45,221(001018,2),221(00904c,51),221(0050f2,2),cap:0011,htcap:000c,htagg:19,htmcs:000000ff,txpow:0f0a|oui:murata':
+        ('', 'Samsung Galaxy S2', '5GHz'),
+    'wifi3|probe:0,1,45,3,221(001018,2),221(00904c,51),htcap:000c,htagg:19,htmcs:000000ff|assoc:0,1,33,36,48,45,221(001018,2),221(00904c,51),221(0050f2,2),cap:0011,htcap:000c,htagg:19,htmcs:000000ff,txpow:0f0a|oui:samsung':
+        ('', 'Samsung Galaxy S2', '5GHz'),
+    'wifi3|probe:0,1,45,3,221(001018,2),221(00904c,51),htcap:000c,htagg:19,htmcs:000000ff|assoc:0,1,33,36,48,45,221(001018,2),221(00904c,51),221(0050f2,2),cap:0011,htcap:000c,htagg:19,htmcs:000000ff,txpow:0f0a|oui:murata':
+        ('', 'Samsung Galaxy S2', '5GHz'),
+    'wifi3|probe:0,1,50,45,221(001018,2),221(00904c,51),htcap:100c,htagg:19,htmcs:000000ff|assoc:0,1,33,36,48,50,45,221(001018,2),221(00904c,51),221(0050f2,2),cap:0431,htcap:100c,htagg:19,htmcs:000000ff,txpow:120a|oui:samsung':
+        ('', 'Samsung Galaxy S2 or Infuse','2.4GHz'),
+    'wifi3|probe:0,1,50,45,221(001018,2),221(00904c,51),htcap:100c,htagg:19,htmcs:000000ff|assoc:0,1,33,36,48,50,45,221(001018,2),221(00904c,51),221(0050f2,2),cap:0431,htcap:100c,htagg:19,htmcs:000000ff,txpow:120a|oui:murata':
+        ('', 'Samsung Galaxy S2 or Infuse','2.4GHz'),
+    'wifi3|probe:0,1,50,45,3,221(001018,2),221(00904c,51),htcap:100c,htagg:19,htmcs:000000ff|assoc:0,1,33,36,48,50,45,221(001018,2),221(00904c,51),221(0050f2,2),cap:0431,htcap:100c,htagg:19,htmcs:000000ff,txpow:120a|oui:samsung':
+        ('', 'Samsung Galaxy S2 or Infuse','2.4GHz'),
+    'wifi3|probe:0,1,50,45,3,221(001018,2),221(00904c,51),htcap:100c,htagg:19,htmcs:000000ff|assoc:0,1,33,36,48,50,45,221(001018,2),221(00904c,51),221(0050f2,2),cap:0431,htcap:100c,htagg:19,htmcs:000000ff,txpow:120a|oui:murata':
+        ('', 'Samsung Galaxy S2 or Infuse','2.4GHz'),
+
     'wifi3|probe:0,1,45,3,221(001018,2),221(00904c,51),htcap:000c,htagg:19,htmcs:000000ff|assoc:0,1,33,36,48,45,221(001018,2),221(00904c,51),221(0050f2,2),cap:0011,htcap:000c,htagg:19,htmcs:000000ff,txpow:0f09|oui:samsung':
         ('', 'Samsung Galaxy S2+', '5GHz'),
     'wifi3|probe:0,1,45,3,221(001018,2),221(00904c,51),htcap:000c,htagg:19,htmcs:000000ff|assoc:0,1,33,36,48,45,221(001018,2),221(00904c,51),221(0050f2,2),cap:0011,htcap:000c,htagg:19,htmcs:000000ff,txpow:0f09|oui:murata':
@@ -712,6 +733,10 @@
         ('BCM4335', 'Samsung Galaxy S4', '2.4GHz'),
     'wifi3|probe:0,1,50,45,127,221(001018,2),221(00904c,51),221(00904c,4),221(0050f2,8),htcap:102d,htagg:17,htmcs:000000ff,extcap:0000080000000040|assoc:0,1,33,36,48,50,45,221(001018,2),221(0050f2,2),cap:0431,htcap:102d,htagg:17,htmcs:000000ff,txpow:1201|oui:murata':
         ('BCM4335', 'Samsung Galaxy S4', '2.4GHz'),
+    'wifi3|probe:0,1,50,45,127,107,221(506f9a,16),221(001018,2),221(00904c,51),221(00904c,4),221(0050f2,8),htcap:102d,htagg:17,htmcs:000000ff,intwrk:0f,extcap:0000088000400040|assoc:0,1,33,36,48,50,45,127,107,221(001018,2),221(0050f2,2),cap:0431,htcap:102d,htagg:17,htmcs:000000ff,txpow:1201,intwrk:0f,extcap:000000800040|oui:samsung':
+        ('BCM4335', 'Samsung Galaxy S4', '2.4GHz'),
+    'wifi3|probe:0,1,50,45,127,107,221(506f9a,16),221(001018,2),221(00904c,51),221(00904c,4),221(0050f2,8),htcap:102d,htagg:17,htmcs:000000ff,intwrk:0f,extcap:0000088000400040|assoc:0,1,33,36,48,50,45,127,107,221(001018,2),221(0050f2,2),cap:0431,htcap:102d,htagg:17,htmcs:000000ff,txpow:1201,intwrk:0f,extcap:000000800040|oui:murata':
+        ('BCM4335', 'Samsung Galaxy S4', '2.4GHz'),
 
     'wifi3|probe:0,1,45,127,107,191,221(506f9a,16),221(00904c,4),221(0050f2,8),221(001018,2),htcap:006f,htagg:17,htmcs:0000ffff,vhtcap:0f815832,vhtrxmcs:0000fffa,vhttxmcs:0000fffa,intwrk:0f,extcap:0000088001400040|assoc:0,1,33,36,48,45,127,107,191,221(00904c,4),221(001018,2),221(0050f2,2),cap:0011,htcap:006f,htagg:17,htmcs:0000ffff,vhtcap:0f815832,vhtrxmcs:0000fffa,vhttxmcs:0000fffa,txpow:e20b,intwrk:0f,extcap:0000088001400040|oui:samsung':
         ('BCM4354', 'Samsung Galaxy S5', '5GHz'),
diff --git a/waveguide/fake/taxonomy/9c:d9:17:00:00:02 b/waveguide/fake/taxonomy/9c:d9:17:00:00:02
index 43be77d..8d6402c 100644
--- a/waveguide/fake/taxonomy/9c:d9:17:00:00:02
+++ b/waveguide/fake/taxonomy/9c:d9:17:00:00:02
@@ -1 +1 @@
-wifi|probe:0,1,45,221(0050f2,8),191,htcap:016e,vhtcap:31800120|assoc:0,1,33,36,48,45,221(0050f2,2),191,127,htcap:016e,vhtcap:31800120
+wifi3|probe:0,1,50,3,45,221(0050f2,8),htcap:012c,htagg:03,htmcs:000000ff|assoc:0,1,50,48,45,221(0050f2,2),127,cap:0431,htcap:012c,htagg:03,htmcs:000000ff,extcap:00000a02
diff --git a/wifi/quantenna.py b/wifi/quantenna.py
index 7ad4079..10af967 100755
--- a/wifi/quantenna.py
+++ b/wifi/quantenna.py
@@ -9,29 +9,52 @@
 import utils
 
 
+ALREADY_MEMBER_FMT = ('device %s is already a member of a bridge; '
+                      "can't enslave it to bridge %s.")
+NOT_MEMBER_FMT = 'device %s is not a slave of %s'
+
+
 def _get_interface():
-  return subprocess.check_output(['get-quantenna-interface']).strip() or None
+  return subprocess.check_output(['get-quantenna-interface']).strip()
 
 
-def _get_qcsapi():
-  # qcsapi_pcie_static runs on PCIe hosts, e.g. GFRG250.
-  # call_qcsapi runs on the LHOST, e.g. GFEX250.
-  return next((qcsapi for qcsapi in ['qcsapi_pcie_static', 'call_qcsapi']
-               if utils.subprocess_quiet(['runnable', qcsapi]) == 0), None)
-
-
-def _get_mac_address():
-  var = {'wlan0': 'MAC_ADDR_WIFI', 'wlan1': 'MAC_ADDR_WIFI2'}[_get_interface()]
+def _get_mac_address(interface):
+  try:
+    var = {'wlan0': 'MAC_ADDR_WIFI', 'wlan1': 'MAC_ADDR_WIFI2'}[interface]
+  except KeyError:
+    raise utils.BinWifiException('no MAC address for %s in hnvram' % interface)
   return subprocess.check_output(['hnvram', '-rq', var]).strip()
 
 
 def _qcsapi(*args):
-  return subprocess.check_output([_get_qcsapi()] + list(args)).strip()
+  return subprocess.check_output(['qcsapi'] + list(args)).strip()
+
+
+def _brctl(*args):
+  return subprocess.check_output(['brctl'] + list(args),
+                                 stderr=subprocess.STDOUT).strip()
+
+
+def _set_interface_in_bridge(bridge, interface, want_in_bridge):
+  """Add/remove Quantenna interface from/to the bridge."""
+  if want_in_bridge:
+    command = 'addif'
+    error_fmt = ALREADY_MEMBER_FMT
+  else:
+    command = 'delif'
+    error_fmt = NOT_MEMBER_FMT
+
+  try:
+    _brctl(command, bridge, interface)
+  except subprocess.CalledProcessError as e:
+    if error_fmt % (interface, bridge) not in e.output:
+      raise utils.BinWifiException(e.output)
 
 
 def _set(mode, opt):
   """Enable wifi."""
-  if not _get_interface() or not _get_qcsapi():
+  interface = _get_interface()
+  if not interface:
     return False
 
   _qcsapi('rfenable', '0')
@@ -47,7 +70,7 @@
   for param, value in config.iteritems():
     _qcsapi('update_config_param', 'wifi0', param, value)
 
-  _qcsapi('set_mac_addr', 'wifi0', _get_mac_address())
+  _qcsapi('set_mac_addr', 'wifi0', _get_mac_address(interface))
 
   if int(_qcsapi('is_startprod_done')):
     _qcsapi('reload_in_mode', 'wifi0', mode)
@@ -61,12 +84,14 @@
       raise utils.BinWifiException('startprod timed out')
 
   if mode == 'ap':
+    _set_interface_in_bridge(opt.bridge, interface, True)
     _qcsapi('set_ssid', 'wifi0', opt.ssid)
     _qcsapi('set_passphrase', 'wifi0', '0', os.environ['WIFI_PSK'])
     _qcsapi('set_option', 'wifi0', 'ssid_broadcast',
             '0' if opt.hidden_mode else '1')
     _qcsapi('rfenable', '1')
   elif mode == 'sta':
+    _set_interface_in_bridge(opt.bridge, interface, False)
     _qcsapi('create_ssid', 'wifi0', opt.ssid)
     _qcsapi('ssid_set_passphrase', 'wifi0', opt.ssid, '0',
             os.environ['WIFI_CLIENT_PSK'])
@@ -79,7 +104,7 @@
 
 def _stop(_):
   """Disable wifi."""
-  if not _get_interface() or not _get_qcsapi():
+  if not _get_interface():
     return False
 
   _qcsapi('rfenable', '0')
diff --git a/wifi/quantenna_test.py b/wifi/quantenna_test.py
index 1f99d41..72f0333 100755
--- a/wifi/quantenna_test.py
+++ b/wifi/quantenna_test.py
@@ -3,6 +3,8 @@
 """Tests for quantenna.py."""
 
 import os
+from subprocess import CalledProcessError
+
 from configs_test import FakeOptDict
 import quantenna
 from wvtest import wvtest
@@ -17,14 +19,39 @@
     return '1\n' if ['startprod', 'wifi0'] in calls else '0\n'
 
 
-def set_fakes(interface='wlan1', qcsapi='qcsapi_pcie_static'):
+bridge_interfaces = set()
+
+
+def fake_brctl(*args):
+  bridge = args[-2]
+  wvtest.WVPASS(bridge == 'br0')
+  interface = args[-1]
+  if 'addif' in args:
+    if interface in bridge_interfaces:
+      raise CalledProcessError(
+          returncode=1, cmd=['brctl'] + list(args),
+          output=quantenna.ALREADY_MEMBER_FMT % (interface, bridge))
+    bridge_interfaces.add(interface)
+    return
+
+  if 'delif' in args:
+    if interface not in bridge_interfaces:
+      raise CalledProcessError(
+          returncode=1, cmd=['brctl'] + list(args),
+          output=quantenna.NOT_MEMBER_FMT % (interface, bridge))
+    bridge_interfaces.remove(interface)
+    return
+
+
+def set_fakes(interface='wlan1'):
   del calls[:]
+  bridge_interfaces.clear()
   os.environ['WIFI_PSK'] = 'wifi_psk'
   os.environ['WIFI_CLIENT_PSK'] = 'wifi_client_psk'
   quantenna._get_interface = lambda: interface
-  quantenna._get_qcsapi = lambda: qcsapi
-  quantenna._get_mac_address = lambda: '00:11:22:33:44:55'
+  quantenna._get_mac_address = lambda _: '00:11:22:33:44:55'
   quantenna._qcsapi = fake_qcsapi
+  quantenna._brctl = fake_brctl
 
 
 def matching_calls_indices(accept):
@@ -40,13 +67,12 @@
   wvtest.WVFAIL(quantenna.stop_ap_wifi(opt))
   wvtest.WVFAIL(quantenna.stop_client_wifi(opt))
   wvtest.WVPASSEQ(calls, [])
-  set_fakes(qcsapi='')
   wvtest.WVFAIL(quantenna.set_wifi(opt))
   wvtest.WVFAIL(quantenna.set_client_wifi(opt))
   wvtest.WVFAIL(quantenna.stop_ap_wifi(opt))
   wvtest.WVFAIL(quantenna.stop_client_wifi(opt))
   wvtest.WVPASSEQ(calls, [])
-  set_fakes(interface='', qcsapi='')
+  set_fakes(interface='')
   wvtest.WVFAIL(quantenna.set_wifi(opt))
   wvtest.WVFAIL(quantenna.set_client_wifi(opt))
   wvtest.WVFAIL(quantenna.stop_ap_wifi(opt))
@@ -57,10 +83,12 @@
 @wvtest.wvtest
 def set_wifi_test():
   opt = FakeOptDict()
+  opt.bridge = 'br0'
   set_fakes()
 
   # Run set_wifi for the first time.
   wvtest.WVPASS(quantenna.set_wifi(opt))
+  wvtest.WVPASS('wlan1' in bridge_interfaces)
 
   # 'rfenable 0' must be run first so that a live interface is not being
   # modified.
@@ -104,6 +132,7 @@
   opt.width = '80'
   new_calls_start = len(calls)
   wvtest.WVPASS(quantenna.set_client_wifi(opt))
+  wvtest.WVFAIL('wlan1' in bridge_interfaces)
 
   # Clear old calls.
   del calls[:new_calls_start]
@@ -143,6 +172,13 @@
   wvtest.WVPASSLT(rim, i[0])
   wvtest.WVPASSLT(i[-1], calls.index(['apply_security_config', 'wifi0']))
 
+  # Make sure subsequent equivalent calls don't fail despite the redundant
+  # bridge changes.
+  wvtest.WVPASS(quantenna.set_client_wifi(opt))
+  wvtest.WVPASS(quantenna.set_client_wifi(opt))
+  wvtest.WVPASS(quantenna.set_wifi(opt))
+  wvtest.WVPASS(quantenna.set_wifi(opt))
+
 
 @wvtest.wvtest
 def stop_wifi_test():