taxonomy: add dnssdmon.

Depending on specific apps installed, iOS devices will
periodically send DNS-SD cache updates which include TXT fields
in the Additional Records containing model information. Watch
for these messages for an extended period of time, half an hour
by default, then print them out and exit. It is expected to be
run in a loop in this way.

Apple uses several different model numbering schemes in these
announcements: https://www.theiphonewiki.com/wiki/Category:Devices
Provide a hash table to map from these identifiers back to a
meaningful model name.

Change-Id: I167dd2b051ab8f3e80923cfe543bd83ddad90958
diff --git a/cmds/Makefile b/cmds/Makefile
index 134ca85..70ebdcb 100644
--- a/cmds/Makefile
+++ b/cmds/Makefile
@@ -55,7 +55,8 @@
 	is-secure-boot
 ARCH_TARGETS=\
 
-ifeq ($(BUILD_ASUS),y)
+ifeq ($(BUILD_LIBNL_UTILS),y)
+ARCH_TARGETS += dnssdmon
 TARGETS += asustax
 HOST_TEST_TARGETS += host-asustax_test
 endif
@@ -235,6 +236,14 @@
 dhcpvendorlookup.o: CFLAGS += -Wno-missing-field-initializers
 host-dhcpvendorlookup.o: CFLAGS += -Wno-missing-field-initializers
 host-dhcpvendortax: host-dhcpvendortax.o host-dhcpvendorlookup.o
+dnssdmon: dnssdmon.o l2utils.o modellookup.o
+dnssdmon: LIBS += -lnl-3 -lstdc++ -lm -lresolv
+modellookup.c: modellookup.gperf
+	$(GPERF) -G -C -t -T -L ANSI-C -n -N model_lookup -K model --delimiters="|" \
+		--includes --output-file=modellookup.c modellookup.gperf
+modellookup.o: CFLAGS += -Wno-missing-field-initializers
+host-modellookup.o: CFLAGS += -Wno-missing-field-initializers
+
 
 TESTS = $(wildcard test-*.sh) $(wildcard test-*.py) $(wildcard *_test.py) $(TEST_TARGETS)
 ifeq ($(RUN_HOST_TESTS),y)
diff --git a/cmds/dnssdmon.cc b/cmds/dnssdmon.cc
new file mode 100644
index 0000000..f598d9a
--- /dev/null
+++ b/cmds/dnssdmon.cc
@@ -0,0 +1,427 @@
+/*
+ * Copyright 2016 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.
+ */
+
+/*
+ * dnssdmon
+ * Listen for DNS-SD packets containing TXT fields which help to identify
+ * the device. For example, some iOS devices send a model string:
+ *    My iPad._device-info._tcp.local: type TXT, class IN, cache flush
+ *          Name: My iPad._device-info._tcp.local
+ *          Type: TXT (Text strings) (16)
+ *          .000 0000 0000 0001 = Class: IN (0x0001)
+ *          1... .... .... .... = Cache flush: True
+ *          Time to live: 4500
+ *          Data length: 12
+ *          TXT Length: 11
+ *          TXT: model=J81AP
+ */
+
+#include <arpa/inet.h>
+#include <arpa/nameser.h>
+#include <assert.h>
+#include <ctype.h>
+#include <net/if.h>
+#include <resolv.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/ioctl.h>
+#include <sys/select.h>
+#include <time.h>
+#include <unistd.h>
+
+#include <iostream>
+#include <string>
+#include <tr1/unordered_map>
+
+#include "l2utils.h"
+#include "modellookup.h"
+
+
+#define MDNS_PORT 5353
+#define MDNS_IPV4 "224.0.0.251"
+#define MDNS_IPV6 "ff02::fb"
+#define MIN(a,b) (((a)<(b))?(a):(b))
+
+
+typedef std::tr1::unordered_map<std::string, std::string> HostsMapType;
+HostsMapType hosts;
+
+
+/* Return monotonically increasing time in seconds. */
+static time_t monotime(void) {
+  struct timespec ts;
+  clock_gettime(CLOCK_MONOTONIC, &ts);
+  return ts.tv_sec;
+}
+
+
+static void strncpy_limited(char *dst, size_t dstlen,
+    const char *src, size_t srclen)
+{
+  size_t i;
+  size_t lim = (srclen >= (dstlen - 1)) ? (dstlen - 2) : srclen;
+
+  for (i = 0; i < lim; ++i) {
+    unsigned char s = src[i];
+    if (isspace(s) || s == ';') {
+      dst[i] = ' ';  // deliberately convert newline to space
+    } else if (isprint(s)) {
+      dst[i] = s;
+    } else {
+      dst[i] = '_';
+    }
+  }
+  dst[lim] = '\0';
+}
+
+
+void add_hostmap_entry(std::string macaddr, std::string model)
+{
+  HostsMapType::iterator found = hosts.find(macaddr);
+  if (found != hosts.end()) {
+    return;
+  }
+
+  hosts[macaddr] = model;
+}
+
+
+int get_ifindex(const char *ifname)
+{
+  int fd;
+  struct ifreq ifr;
+  size_t nlen = strlen(ifname);
+
+  if ((fd = socket(AF_PACKET, SOCK_DGRAM, 0)) < 0) {
+    perror("ERR: socket");
+    exit(1);
+  }
+
+  if (nlen >= sizeof(ifr.ifr_name)) {
+    fprintf(stderr, "ERR: interface name %s is too long\n", ifname);
+    exit(1);
+  }
+
+  memset(&ifr, 0, sizeof(ifr));
+  strncpy(ifr.ifr_name, ifname, nlen);
+  ifr.ifr_name[nlen] = '\0';
+
+  if (ioctl(fd, SIOCGIFINDEX, &ifr) < 0) {
+    perror("ERR: SIOCGIFINDEX");
+    exit(1);
+  }
+
+  close(fd);
+  return ifr.ifr_ifindex;
+}  /* get_ifindex */
+
+
+void init_mdns_socket_common(int s, const char *ifname)
+{
+  struct ifreq ifr;
+  unsigned int enable = 1;
+
+  memset(&ifr, 0, sizeof(ifr));
+  snprintf(ifr.ifr_name, sizeof(ifr.ifr_name), "%s", ifname);
+  if (setsockopt(s, SOL_SOCKET, SO_BINDTODEVICE, &ifr, sizeof(ifr))) {
+    perror("ERR: SO_BINDTODEVICE");
+    exit(1);
+  }
+
+  if (setsockopt(s, SOL_SOCKET, SO_REUSEADDR, &enable, sizeof(enable)) < 0) {
+    perror("ERR: SO_REUSEADDR");
+    exit(1);
+  }
+}
+
+
+int init_mdns_socket_ipv4(const char *ifname)
+{
+  int s;
+  struct sockaddr_in sin;
+  struct ip_mreq mreq;
+
+  if ((s = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP)) < 0) {
+    perror("ERR: socket");
+    exit(1);
+  }
+  init_mdns_socket_common(s, ifname);
+
+  memset(&sin, 0, sizeof(sin));
+  sin.sin_family = AF_INET;
+  sin.sin_port = htons(MDNS_PORT);
+  sin.sin_addr.s_addr = htonl(INADDR_ANY);
+  if (bind(s, (struct sockaddr *)&sin, sizeof(sin))) {
+    perror("ERR: bind");
+    exit(1);
+  }
+
+  memset(&mreq, 0, sizeof(mreq));
+  mreq.imr_interface.s_addr = htonl(INADDR_ANY);
+  if (inet_pton(AF_INET, MDNS_IPV4, &mreq.imr_multiaddr) != 1) {
+    fprintf(stderr, "ERR: inet_pton(%s)", MDNS_IPV4);
+    exit(1);
+  }
+  if (setsockopt(s, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mreq, sizeof(mreq)) < 0) {
+    perror("ERR: setsockopt(IP_ADD_MEMBERSHIP)");
+    exit(1);
+  }
+
+  return s;
+}
+
+
+int init_mdns_socket_ipv6(const char *ifname, int ifindex)
+{
+  int s;
+  struct sockaddr_in6 sin6;
+  struct ipv6_mreq mreq;
+  int off = 0;
+
+  if ((s = socket(AF_INET6, SOCK_DGRAM, IPPROTO_UDP)) < 0) {
+    perror("ERR: socket(AF_INET6)");
+    exit(1);
+  }
+  init_mdns_socket_common(s, ifname);
+
+  if (setsockopt(s, IPPROTO_IPV6, IPV6_V6ONLY, (void *)&off, sizeof(off))) {
+    perror("ERR: setsockopt(IPV6_V6ONLY)");
+    exit(1);
+  }
+
+  memset(&sin6, 0, sizeof(sin6));
+  sin6.sin6_family = AF_INET6;
+  sin6.sin6_port = htons(MDNS_PORT);
+  sin6.sin6_addr = in6addr_any;
+  if (bind(s, (struct sockaddr *)&sin6, sizeof(sin6))) {
+    perror("ERR: bind");
+    exit(1);
+  }
+
+  memset(&mreq, 0, sizeof(mreq));
+  mreq.ipv6mr_interface = ifindex;
+  if (inet_pton(AF_INET6, MDNS_IPV6, &mreq.ipv6mr_multiaddr) != 1) {
+    fprintf(stderr, "ERR: inet_pton(%s) failed", MDNS_IPV6);
+    exit(1);
+  }
+  if (setsockopt(s, IPPROTO_IPV6, IPV6_JOIN_GROUP, &mreq, sizeof(mreq)) < 0) {
+    perror("ERR: setsockopt(IPV6_JOIN_GROUP)");
+    exit(1);
+  }
+
+  return s;
+}
+
+
+/*
+ * Search a DNS TXT record for fields which look like a model description.
+ * MacOS and iOS devices send "model=...", various peripherals send
+ * "ty=..."  (presumably for 'type').
+ */
+int parse_txt_for_model(const unsigned char *rdata, unsigned int rdlen,
+    char *model, unsigned int modellen)
+{
+  const unsigned char *p = rdata;
+
+  while (rdlen > 0) {
+    unsigned int txtlen = p[0];
+    /*
+     * TXT record format is:
+     * Length1 (1 byte)
+     * String1 (variable length)
+     * Length2 (1 byte)
+     * String2 (variable length)
+     * etc.
+     */
+    p++;
+    rdlen--;
+    if (txtlen > rdlen) {
+      fprintf(stderr, "ERR: Malformed TXT record\n");
+      return -1;
+    }
+
+    if (txtlen > 6 && strncmp((const char *)p, "model=", 6) == 0) {
+      strncpy_limited(model, modellen, (const char *)(p + 6), txtlen - 6);
+      return 0;
+    }
+    if (txtlen > 3 && strncmp((const char *)p, "ty=", 3) == 0) {
+      strncpy_limited(model, modellen, (const char *)(p + 3), txtlen - 3);
+      return 0;
+    }
+    rdlen -= txtlen;
+    p += txtlen;
+  }
+
+  return 1;
+}
+
+void process_mdns(int s)
+{
+  ssize_t len;
+  ns_msg msg;
+  unsigned int i, n, rr_count;
+  ns_sect sections[] = {ns_s_an, ns_s_ar};  // Answers and Additional Records
+  struct sockaddr_storage from;
+  socklen_t fromlen = sizeof(from);
+  char ipstr[INET6_ADDRSTRLEN];
+  uint8_t buf[4096];
+
+  if ((len = recvfrom(s, buf, sizeof(buf), 0,
+      (struct sockaddr *)&from, &fromlen)) < 0) {
+    return;
+  }
+
+  if (from.ss_family == AF_INET) {
+    inet_ntop(AF_INET, &(((struct sockaddr_in *)&from)->sin_addr),
+        ipstr, sizeof(ipstr));
+  } else if (from.ss_family == AF_INET6) {
+    inet_ntop(AF_INET6, &(((struct sockaddr_in6 *)&from)->sin6_addr),
+        ipstr, sizeof(ipstr));
+  }
+
+  if (ns_initparse(buf, len, &msg) < 0) {
+    fprintf(stderr, "ERR: ns_initparse\n");
+    return;
+  }
+
+  for (i = 0; i < (sizeof(sections) / sizeof(sections[0])); ++i) {
+    ns_sect sect = sections[i];
+    rr_count = ns_msg_count(msg, sect);
+    for (n = 0; n < rr_count; ++n) {
+      ns_rr rr;
+      if (ns_parserr(&msg, sect, n, &rr)) {
+        fprintf(stderr, "ERR: unable to parse RR type=%s n=%d\n",
+            (sect == ns_s_an) ? "ns_s_an" : "ns_s_ar", n);
+        continue;
+      }
+
+      if (ns_rr_type(rr) == ns_t_txt) {
+        const unsigned char *rdata = ns_rr_rdata(rr);
+        unsigned int rdlen = ns_rr_rdlen(rr);
+        char model[64];
+        if (parse_txt_for_model(rdata, rdlen, model, sizeof(model)) == 0) {
+          std::string mac = get_l2addr_for_ip(std::string(ipstr));
+          add_hostmap_entry(mac, model);
+        }
+      }
+    }
+  }
+}
+
+
+void listen_for_mdns(const char *ifname, int ifindex, time_t seconds)
+{
+  int s4, s6;
+  time_t start, now;
+  struct timeval tv;
+  fd_set readfds;
+  int maxfd;
+
+  now = start = monotime();
+  s4 = init_mdns_socket_ipv4(ifname);
+  s6 = init_mdns_socket_ipv6(ifname, ifindex);
+  maxfd = ((s4 > s6) ? s4 : s6) + 1;
+
+  do {
+    FD_ZERO(&readfds);
+    FD_SET(s4, &readfds);
+    FD_SET(s6, &readfds);
+    memset(&tv, 0, sizeof(tv));
+
+    tv.tv_sec = (seconds - (now - start)) + 1;
+    if (select(maxfd, &readfds, NULL, NULL, &tv) < 0) {
+      perror("ERR: select");
+      return;
+    }
+
+    if (FD_ISSET(s4, &readfds)) {
+      process_mdns(s4);
+    }
+    if (FD_ISSET(s6, &readfds)) {
+      process_mdns(s6);
+    }
+
+    now = monotime();
+  } while ((now - start) < seconds);
+}
+
+
+void usage(char *progname)
+{
+  fprintf(stderr, "usage: %s [-i ifname] [-t seconds]\n", progname);
+  fprintf(stderr, "\t-i ifname - interface to use (default: br0)\n");
+  fprintf(stderr, "\t-t seconds - number of seconds to run before exiting.\n");
+  exit(1);
+}
+
+
+int main(int argc, char *argv[])
+{
+  int opt;
+  const char *ifname = "br0";
+  time_t seconds = 30 * 60;
+  int ifindex;
+  L2Map l2map;
+
+  setlinebuf(stdout);
+
+  while ((opt = getopt(argc, argv, "i:t:")) != -1) {
+    switch (opt) {
+      case 'i':
+        ifname = optarg;
+        break;
+      case 't':
+        seconds = atoi(optarg);
+        if (seconds < 0) usage(argv[0]);
+        break;
+      default:
+        usage(argv[0]);
+        break;
+    }
+  }
+
+  alarm(seconds * 2);
+
+  if ((ifindex = get_ifindex(ifname)) < 0) {
+    fprintf(stderr, "ERR: get_ifindex(%s).\n", ifname);
+    exit(1);
+  }
+
+  /* block for an extended period, listening for DNS-SD */
+  listen_for_mdns(ifname, ifindex, seconds);
+
+  for (HostsMapType::const_iterator ii = hosts.begin();
+      ii != hosts.end(); ++ii) {
+    std::string macaddr = ii->first;
+    std::string model = ii->second;
+
+    if (model.size() > 0) {
+      const struct model_strings *l;
+      std::string genus, species;
+
+      genus = species = model;
+      if ((l = model_lookup(model.c_str(), model.length())) != NULL) {
+        genus = l->genus;
+        species = l->species;
+      }
+      std::cout << "dnssd " << macaddr << " "
+        << genus << ";" << species << std::endl;
+    }
+  }
+
+  exit(0);
+}
diff --git a/cmds/l2utils.cc b/cmds/l2utils.cc
index d0549cb..65ce68f 100644
--- a/cmds/l2utils.cc
+++ b/cmds/l2utils.cc
@@ -119,3 +119,24 @@
     close(s);
   }
 }
+
+std::string get_l2addr_for_ip(std::string ipaddr)
+{
+  static L2Map l2map;
+  std::string mac;
+
+  L2Map::const_iterator l2i = l2map.find(ipaddr);
+  if (l2i == l2map.end()) {
+    /* If we only just saw this IP address, it may not be present in
+     * the cached data we have. Refresh the cache and try again. */
+    get_l2_map(&l2map);
+    l2i = l2map.find(ipaddr);
+  }
+  if (l2i == l2map.end()) {
+    mac = std::string("00:00:00:00:00:00");
+  } else {
+    mac = l2i->second;
+  }
+
+  return mac;
+}
diff --git a/cmds/l2utils.h b/cmds/l2utils.h
index 6b2385a..cc48579 100644
--- a/cmds/l2utils.h
+++ b/cmds/l2utils.h
@@ -6,5 +6,6 @@
 
 typedef std::tr1::unordered_map<std::string, std::string> L2Map;
 extern void get_l2_map(L2Map *l2map);
+extern std::string get_l2addr_for_ip(std::string ipaddr);
 
 #endif  // L2UTILS_H
diff --git a/cmds/modellookup.gperf b/cmds/modellookup.gperf
new file mode 100644
index 0000000..774373a
--- /dev/null
+++ b/cmds/modellookup.gperf
@@ -0,0 +1,306 @@
+%{
+/*
+ * Copyright 2016 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 "modellookup.h"
+
+%}
+struct model_strings {
+  char *model;
+  char *genus;
+  char *species;
+};
+%%
+AppleTV1,1| "Apple TV", "Apple TV (1st gen)"
+AppleTV2,1| "Apple TV", "Apple TV (2nd gen)"
+AppleTV3,1| "Apple TV", "Apple TV (3rd gen)"
+AppleTV3,2| "Apple TV", "Apple TV (3rd gen rev A)"
+AppleTV5,3| "Apple TV", "Apple TV (4th gen)"
+iMac,1| "iMac", "iMac G3"
+iMac4,1| "iMac", "iMac Early 2006"
+iMac4,2| "iMac", "iMac Mid 2006"
+iMac5,1| "iMac", "iMac Late 2006"
+iMac5,2| "iMac", "iMac Late 2006"
+iMac6,1| "iMac", "iMac Late 2006"
+iMac7,1| "iMac", "iMac Mid 2007"
+iMac8,1| "iMac", "iMac Early 2008"
+iMac9,1| "iMac", "iMac Early 2009"
+iMac10,1| "iMac", "iMac Late 2009"
+iMac11,1| "iMac", "iMac Late 2009"
+iMac11,2| "iMac", "iMac Mid 2010"
+iMac11,3| "iMac", "iMac Mid 2010"
+iMac12,1| "iMac", "iMac Mid 2011"
+iMac12,2| "iMac", "iMac Mid 2011"
+iMac13,1| "iMac", "iMac Late 2012"
+iMac13,2| "iMac", "iMac Late 2012"
+iMac14,1| "iMac", "iMac Late 2013"
+iMac14,2| "iMac", "iMac Late 2013"
+iMac14,3| "iMac", "iMac Late 2013"
+iMac14,4| "iMac", "iMac Mid 2014"
+iMac15,1| "iMac", "iMac Late 2014"
+iMac16,1| "iMac", "iMac Late 2015"
+iMac16,2| "iMac", "iMac Late 2015"
+iMac17,1| "iMac", "iMac Late 2015"
+iPad1,1| "iPad", "iPad (1st gen)"
+iPad2,1| "iPad", "iPad (2nd gen)"
+iPad2,2| "iPad", "iPad (2nd gen)"
+iPad2,3| "iPad", "iPad (2nd gen)"
+iPad2,4| "iPad", "iPad (2nd gen)"
+iPad2,5| "iPad mini", "iPad mini (1st gen)"
+iPad2,6| "iPad mini", "iPad mini (1st gen)"
+iPad2,7| "iPad mini", "iPad mini (1st gen)"
+iPad3,1| "iPad", "iPad (3rd gen)"
+iPad3,2| "iPad", "iPad (3rd gen)"
+iPad3,3| "iPad", "iPad (3rd gen)"
+iPad3,4| "iPad", "iPad (4th gen)"
+iPad3,5| "iPad", "iPad (4th gen)"
+iPad3,6| "iPad", "iPad (4th gen)"
+iPad4,1| "iPad", "iPad Air (1st gen)"
+iPad4,2| "iPad", "iPad Air (1st gen)"
+iPad4,3| "iPad", "iPad Air (1st gen)"
+iPad4,4| "iPad mini", "iPad mini (2nd gen)"
+iPad4,5| "iPad mini", "iPad mini (2nd gen)"
+iPad4,6| "iPad mini", "iPad mini (2nd gen)"
+iPad4,7| "iPad mini", "iPad mini (3rd gen)"
+iPad4,8| "iPad mini", "iPad mini (3rd gen)"
+iPad4,9| "iPad mini", "iPad mini (3rd gen)"
+iPad5,1| "iPad mini", "iPad mini (4th gen)"
+iPad5,2| "iPad mini", "iPad mini (4th gen)"
+iPad5,3| "iPad", "iPad Air (2nd gen)"
+iPad5,4| "iPad", "iPad Air (2nd gen)"
+iPad6,3| "iPad Pro", "iPad Pro 9.7 inch"
+iPad6,4| "iPad Pro", "iPad Pro 9.7 inch"
+iPad6,7| "iPad Pro", "iPad Pro 12.9 inch"
+iPad6,8| "iPad Pro", "iPad Pro 12.9 inch"
+iPhone1,1| "iPhone", "iPhone 1"
+iPhone1,2| "iPhone 3G", "iPhone 3G"
+iPhone2,1| "iPhone 3GS", "iPhone 3GS"
+iPhone3,1| "iPhone 4", "iPhone 4"
+iPhone3,2| "iPhone 4", "iPhone 4"
+iPhone3,3| "iPhone 4", "iPhone 4"
+iPhone4,1| "iPhone 4S", "iPhone 4S"
+iPhone5,1| "iPhone 5", "iPhone 5"
+iPhone5,2| "iPhone 5", "iPhone 5"
+iPhone5,3| "iPhone 5c", "iPhone 5c"
+iPhone5,4| "iPhone 5c", "iPhone 5c"
+iPhone6,1| "iPhone 5s", "iPhone 5s"
+iPhone6,2| "iPhone 5s", "iPhone 5s"
+iPhone7,1| "iPhone 6+", "iPhone 6+"
+iPhone7,2| "iPhone 6", "iPhone 6"
+iPhone8,1| "iPhone 6s", "iPhone 6s"
+iPhone8,2| "iPhone 6s+", "iPhone 6s+"
+iPhone8,4| "iPhone SE", "iPhone SE"
+iPod1,1| "iPod Touch", "iPod Touch (1st gen)"
+iPod2,1| "iPod Touch", "iPod Touch (2nd gen)"
+iPod3,1| "iPod Touch", "iPod Touch (3rd gen)"
+iPod4,1| "iPod Touch", "iPod Touch (4th gen)"
+iPod5,1| "iPod Touch", "iPod Touch (5th gen)"
+iPod7,1| "iPod Touch", "iPod Touch (6th gen)"
+MacBook1,1| "MacBook", "MacBook Early 2006"
+MacBook2,1| "MacBook", "MacBook Late 2006"
+MacBook3,1| "MacBook", "MacBook Late 2007"
+MacBook4,1| "MacBook", "MacBook Early 2008"
+MacBook4,2| "MacBook", "MacBook Late 2008"
+MacBook5,1| "MacBook", "MacBook Late 2008"
+MacBook5,2| "MacBook", "MacBook Mid 2009"
+MacBook6,1| "MacBook", "MacBook Late 2009"
+MacBook7,1| "MacBook", "MacBook Mid 2010"
+MacBook8,1| "MacBook", "MacBook Early 2015"
+MacBook9,1| "MacBook", "MacBook Early 2016"
+MacBookAir1,1| "MacBook Air", "MacBook Air Early 2008"
+MacBookAir2,1| "MacBook Air", "MacBook Air Mid 2009"
+MacBookAir3,1| "MacBook Air", "MacBook Air Late 2010"
+MacBookAir3,2| "MacBook Air", "MacBook Air Late 2010"
+MacBookAir4,1| "MacBook Air", "MacBook Air Mid 2011"
+MacBookAir4,2| "MacBook Air", "MacBook Air Mid 2011"
+MacBookAir5,1| "MacBook Air", "MacBook Air Mid 2012"
+MacBookAir5,2| "MacBook Air", "MacBook Air Mid 2012"
+MacBookAir6,1| "MacBook Air", "MacBook Air Mid 2013"
+MacBookAir6,2| "MacBook Air", "MacBook Air Mid 2013"
+MacBookAir7,1| "MacBook Air", "MacBook Air Early 2015"
+MacBookAir7,2| "MacBook Air", "MacBook Air Early 2015"
+MacBookPro1,1| "MacBook Pro", "MacBook Pro Early 2006"
+MacBookPro1,2| "MacBook Pro", "MacBook Pro Early 2006"
+MacBookPro2,1| "MacBook Pro", "MacBook Pro Late 2006"
+MacBookPro2,2| "MacBook Pro", "MacBook Pro Late 2006"
+MacBookPro3,1| "MacBook Pro", "MacBook Pro Mid 2007"
+MacBookPro4,1| "MacBook Pro", "MacBook Pro Early 2008"
+MacBookPro5,1| "MacBook Pro", "MacBook Pro Late 2008"
+MacBookPro5,2| "MacBook Pro", "MacBook Pro Early 2009"
+MacBookPro5,3| "MacBook Pro", "MacBook Pro Mid 2009"
+MacBookPro5,4| "MacBook Pro", "MacBook Pro Mid 2009"
+MacBookPro5,5| "MacBook Pro", "MacBook Pro Mid 2009"
+MacBookPro6,1| "MacBook Pro", "MacBook Pro Mid 2010"
+MacBookPro6,2| "MacBook Pro", "MacBook Pro Mid 2010"
+MacBookPro7,1| "MacBook Pro", "MacBook Pro Mid 2010"
+MacBookPro8,1| "MacBook Pro", "MacBook Pro Early 2011"
+MacBookPro8,2| "MacBook Pro", "MacBook Pro Early 2011"
+MacBookPro8,3| "MacBook Pro", "MacBook Pro Early 2011"
+MacBookPro9,1| "MacBook Pro", "MacBook Pro Mid 2012"
+MacBookPro9,2| "MacBook Pro", "MacBook Pro Mid 2012"
+MacBookPro10,1| "MacBook Pro", "MacBook Pro Mid 2012"
+MacBookPro10,2| "MacBook Pro", "MacBook Pro Late 2012"
+MacBookPro11,1| "MacBook Pro", "MacBook Pro Late 2013"
+MacBookPro11,2| "MacBook Pro", "MacBook Pro Late 2013"
+MacBookPro11,3| "MacBook Pro", "MacBook Pro Late 2013"
+MacBookPro11,4| "MacBook Pro", "MacBook Pro Mid 2015"
+MacBookPro11,5| "MacBook Pro", "MacBook Pro Mid 2015"
+MacBookPro12,1| "MacBook Pro", "MacBook Pro Early 2015"
+Macmini1,1| "Mac Mini", "Mac Mini Early 2006"
+Macmini2,1| "Mac Mini", "Mac Mini Mid 2007"
+Macmini3,1| "Mac Mini", "Mac Mini Early 2009"
+Macmini4,1| "Mac Mini", "Mac Mini Mid 2010"
+Macmini5,1| "Mac Mini", "Mac Mini Mid 2011"
+Macmini5,2| "Mac Mini", "Mac Mini Mid 2011"
+Macmini5,3| "Mac Mini", "Mac Mini Mid 2011"
+Macmini6,1| "Mac Mini", "Mac Mini Late 2012"
+Macmini6,2| "Mac Mini", "Mac Mini Late 2012"
+Macmini7,1| "Mac Mini", "Mac Mini Late 2014"
+Macmini7,2| "Mac Mini", "Mac Mini Late 2014"
+Macmini7,3| "Mac Mini", "Mac Mini Late 2014"
+MacPro1,1| "Mac Pro", "Mac Pro Mid 2006"
+MacPro2,1| "Mac Pro", "Mac Pro Mid 2006"
+MacPro3,1| "Mac Pro", "Mac Pro Early 2008"
+MacPro4,1| "Mac Pro", "Mac Pro Early 2009"
+MacPro5,1| "Mac Pro", "Mac Pro Mid 2010"
+MacPro6,1| "Mac Pro", "Mac Pro Late 2013"
+PowerBook1,1| "PowerBook", "PowerBook G3 Mid 1999"
+PowerBook2,1| "iBook", "iBook G3 Mid 1999"
+PowerBook2,2| "iBook", "iBook G3 Late 2000"
+PowerBook3,1| "PowerBook", "PowerBook G3 Early 2000"
+PowerBook3,2| "PowerBook", "PowerBook G4 Early 2001"
+PowerBook3,3| "PowerBook", "PowerBook G4 Late 2001"
+PowerBook3,4| "PowerBook", "PowerBook G4 Early 2002"
+PowerBook3,5| "PowerBook", "PowerBook G4 Late 2002"
+PowerBook4,1| "iBook", "iBook G3 Late 2001"
+PowerBook4,2| "iBook", "iBook G3 Early 2002"
+PowerBook4,3| "iBook", "iBook G3 Mid 2002"
+PowerBook5,1| "PowerBook", "PowerBook G4 Early 2003"
+PowerBook5,2| "PowerBook", "PowerBook G4 Late 2003"
+PowerBook5,3| "PowerBook", "PowerBook G4 Late 2003"
+PowerBook5,4| "PowerBook", "PowerBook G4 Early 2004"
+PowerBook5,5| "PowerBook", "PowerBook G4 Early 2004"
+PowerBook5,6| "PowerBook", "PowerBook G4 Early 2005"
+PowerBook5,7| "PowerBook", "PowerBook G4 Early 2005"
+PowerBook5,8| "PowerBook", "PowerBook G4 Late 2005"
+PowerBook5,9| "PowerBook", "PowerBook G4 Late 2005"
+PowerBook6,1| "PowerBook", "PowerBook G4 Early 2003"
+PowerBook6,2| "PowerBook", "PowerBook G4 Late 2003"
+PowerBook6,3| "iBook", "iBook G4 Late 2003"
+PowerBook6,4| "PowerBook", "PowerBook G4 Early 2004"
+PowerBook6,5| "iBook", "iBook G4 Early 2004"
+PowerBook6,7| "iBook", "iBook G4 Mid 2005"
+PowerBook6,8| "PowerBook", "PowerBook G4 Early 2005"
+PowerMac1,1| "Power Mac", "Power Mac G3 Early 1999"
+PowerMac1,2| "Power Mac", "Power Mac G4 Late 1999"
+PowerMac2,1| "iMac", "iMac G3 Early 2000"
+PowerMac2,2| "iMac", "iMac G3 Summer 2000"
+PowerMac3,1| "Power Mac", "Power Mac G4 Late 1999"
+PowerMac3,3| "Power Mac", "Power Mac G4 Mid 2000"
+PowerMac3,4| "Power Mac", "Power Mac G4 Early 2001"
+PowerMac3,5| "Power Mac", "Power Mac G4 Mid 2001"
+PowerMac3,6| "Power Mac", "Power Mac G4 Mid 2002"
+PowerMac4,1| "iMac", "iMac G3 Early 2001"
+PowerMac4,2| "iMac", "iMac G4 Early 2002"
+PowerMac4,4| "eMac", "eMac G4 Mid 2002"
+PowerMac4,5| "iMac", "iMac G4 Mid 2002"
+PowerMac5,1| "Power Mac", "Power Mac G4 Cube Mid 2000"
+PowerMac6,1| "iMac", "iMac G4 Early 2003"
+PowerMac6,3| "iMac", "iMac G4 Late 2003"
+PowerMac6,4| "eMac", "eMac G4 Early 2004"
+PowerMac7,2| "Power Mac", "Power Mac G5 Mid 2003"
+PowerMac7,3| "Power Mac", "Power Mac G5 Mid 2004"
+PowerMac8,1| "iMac", "iMac G5 Mid 2004"
+PowerMac8,2| "iMac", "iMac G5 Mid 2005"
+PowerMac9,1| "Power Mac", "Power Mac G5 Late 2004"
+PowerMac10,1| "Mac Mini", "Mac Mini Early 2005"
+PowerMac10,2| "Mac Mini", "Mac Mini Late 2005"
+PowerMac11,2| "Power Mac", "Power Mac G5 Late 2005"
+PowerMac12,1| "iMac", "iMac G5 Late 2005"
+RackMac1,1| "Xserve", "Xserve G4 Mid 2002"
+RackMac1,2| "Xserve", "Xserve G4 Early 2003"
+RackMac3,1| "Xserve", "Xserve G5 Early 2004"
+Watch1,1| "Apple Watch", "Apple Watch"
+Watch1,2| "Apple Watch", "Apple Watch"
+Xserve1,1| "Xserve", "Xserve Xeon Late 2006"
+Xserve2,1| "Xserve", "Xserve Xeon Early 2008"
+Xserve3,1| "Xserve", "Xserve Xeon Early 2009"
+J1AP| "iPad", "iPad (3rd gen)"
+J2AP| "iPad", "iPad (3rd gen)"
+J2AAP| "iPad", "iPad (3rd gen)"
+J127AP| "iPad Pro", "iPad Pro"
+J128AP| "iPad Pro", "iPad Pro"
+J33AP| "Apple TV", "Apple TV (3rd gen)"
+J33iAP| "Apple TV", "Apple TV (3rd gen rev A)"
+J42dAP| "Apple TV", "Apple TV (4th gen)"
+J71AP| "iPad", "iPad Air"
+J72AP| "iPad", "iPad Air"
+J73AP| "iPad", "iPad Air"
+J81AP| "iPad", "iPad Air (2nd gen)"
+J82AP| "iPad", "iPad Air (2nd gen)"
+J85AP| "iPad mini", "iPad mini (2nd gen)"
+J85mAP| "iPad mini", "iPad mini (3rd gen)"
+J86AP| "iPad mini", "iPad mini (2nd gen)"
+J86mAP| "iPad mini", "iPad mini (3rd gen)"
+J87AP| "iPad mini", "iPad mini (2nd gen)"
+J87mAP| "iPad mini", "iPad mini (3rd gen)"
+J96AP| "iPad mini", "iPad mini (4th gen)"
+J97AP| "iPad mini", "iPad mini (4th gen)"
+J98aAP| "iPad Pro", "iPad Pro"
+J99aAP| "iPad Pro", "iPad Pro"
+K48AP| "iPad", "iPad (1st gen)"
+K66AP| "Apple TV", "Apple TV (2nd gen)"
+K93AAP| "iPad", "iPad (2nd gen)"
+K93AP| "iPad", "iPad (2nd gen)"
+K94AP| "iPad", "iPad (2nd gen)"
+K95AP| "iPad", "iPad (2nd gen)"
+M68AP| "iPhone", "iPhone 1"
+N27aAP| "Apple Watch", "Apple Watch"
+N28aAP| "Apple Watch", "Apple Watch"
+N102AP| "iPod Touch", "iPod Touch (6th gen)"
+N18AP| "iPod Touch", "iPod Touch (3rd gen)"
+N41AP| "iPhone 5", "iPhone 5"
+N42AP| "iPhone 5", "iPhone 5"
+N45AP| "iPod Touch", "iPod Touch (1st gen)"
+N48AP| "iPhone 5c", "iPhone 5c"
+N49AP| "iPhone 5c", "iPhone 5c"
+N51AP| "iPhone 5s", "iPhone 5s"
+N53AP| "iPhone 5s", "iPhone 5s"
+N56AP| "iPhone 6+", "iPhone 6+"
+N61AP| "iPhone 6", "iPhone 6"
+N66AP| "iPhone 6s+", "iPhone 6s+"
+N66mAP| "iPhone 6s+", "iPhone 6s+"
+N69AP| "iPhone SE", "iPhone SE"
+N69uAP| "iPhone SE", "iPhone SE"
+N71AP| "iPhone 6s", "iPhone 6s"
+N71mAP| "iPhone 6s", "iPhone 6s"
+N72AP| "iPod Touch", "iPod Touch (2nd gen)"
+N78aAP| "iPod Touch", "iPod Touch (5th gen)"
+N78AP| "iPod Touch", "iPod Touch (5th gen)"
+N81AP| "iPod Touch", "iPod Touch (4th gen)"
+N82AP| "iPhone 3G", "iPhone 3G"
+N88AP| "iPhone 3GS", "iPhone 3GS"
+N90AP| "iPhone 4", "iPhone 4"
+N90BAP| "iPhone 4", "iPhone 4"
+N92AP| "iPhone 4", "iPhone 4"
+N94AP| "iPhone 4S", "iPhone 4S"
+P101AP| "iPad", "iPad (4th gen)"
+P102AP| "iPad", "iPad (4th gen)"
+P103AP| "iPad", "iPad (4th gen)"
+P105AP| "iPad mini", "iPad mini (1st gen)"
+P106AP| "iPad mini", "iPad mini (1st gen)"
+P107AP| "iPad mini", "iPad mini (1st gen)"
+%%
diff --git a/cmds/modellookup.h b/cmds/modellookup.h
new file mode 100644
index 0000000..cd6e7f3
--- /dev/null
+++ b/cmds/modellookup.h
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2016 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.
+ */
+
+
+#ifndef MODELLOOKUP_H_
+#define MODELLOOKUP_H_
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/* Structure returned by the model_lookup function. */
+struct model_strings {
+  const char *model;
+  const char *genus;
+  const char *species;
+};
+
+/* Function generated by gperf for the model_lookup table. */
+extern const struct model_strings *model_lookup(const char *str,
+    unsigned int len);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif  // MODELLOOKUP_H_