Merge "add c version of http_bouncer"
diff --git a/Makefile b/Makefile
index e22fd32..2c82d1e 100644
--- a/Makefile
+++ b/Makefile
@@ -20,7 +20,7 @@
 # via buildroot/packages/google/google_platform/google_platform.mk
 DIRS=libstacktrace ginstall cmds \
 	antirollback tvstat gpio-mailbox spectralanalyzer wifi wifiblaster \
-	sysvar py_mtd
+	sysvar py_mtd devcert
 
 ifeq ($(BUILD_SYSMGR),y)
 DIRS+=sysmgr
diff --git a/devcert/Makefile b/devcert/Makefile
new file mode 100644
index 0000000..cd01a96
--- /dev/null
+++ b/devcert/Makefile
@@ -0,0 +1,17 @@
+default: all
+
+all:
+
+tests: all
+
+install:
+	@echo "Nothing to install."
+
+install-libs:
+	@echo "No libs to install."
+
+test: tests
+	wvtest/wvtestrun ./gfiber-device-cert.test
+
+clean:
+	rm -f *~ .*~
diff --git a/devcert/debian/.gitignore b/devcert/debian/.gitignore
new file mode 100644
index 0000000..8ff0706
--- /dev/null
+++ b/devcert/debian/.gitignore
@@ -0,0 +1,6 @@
+/files
+/changelog
+/*.debhelper
+/gfiber-device-cert.debhelper.log
+/gfiber-device-cert.substvars
+/gfiber-device-cert
diff --git a/devcert/debian/README.Debian b/devcert/debian/README.Debian
new file mode 100644
index 0000000..0085961
--- /dev/null
+++ b/devcert/debian/README.Debian
@@ -0,0 +1,13 @@
+GFiber device certificates
+--------------------------
+
+This package ensures that there's a GFiber client certificate in
+/etc/ssl/certs/device.pem (and associated private key in
+/etc/ssl/private/device.key).  If one isn't already there, it will self-sign
+a certificate and install it for you.
+
+You can dump the contents of the certificate by running gfiber-device-cert
+with no parameters, or add -f to force regeneration of the cert.
+
+Find the source here:
+https://gfiber.googlesource.com/vendor/google/platform/+/master/devcert/
diff --git a/devcert/debian/clean b/devcert/debian/clean
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/devcert/debian/clean
diff --git a/devcert/debian/compat b/devcert/debian/compat
new file mode 100644
index 0000000..7f8f011
--- /dev/null
+++ b/devcert/debian/compat
@@ -0,0 +1 @@
+7
diff --git a/devcert/debian/control b/devcert/debian/control
new file mode 100644
index 0000000..95cd824
--- /dev/null
+++ b/devcert/debian/control
@@ -0,0 +1,14 @@
+Source: gfiber-device-cert
+Section: net
+Priority: extra
+Maintainer: Avery Pennarun <apenwarr@google.com>
+Build-Depends: debhelper (>= 7.0.50~)
+Standards-Version: 3.9.2
+Homepage: https://gfiber.googlesource.com/vendor/google/platform/+/master/devcert/
+Vcs-Git: https://gfiber.googlesource.com/vendor/google/platform
+Vcs-Browser: https://gfiber.googlesource.com/vendor/google/platform
+
+Package: gfiber-device-cert
+Architecture: all
+Depends: openssl (>= 1.0.1)
+Description: Tool for managing GFiber client device certificates
diff --git a/devcert/debian/copyright b/devcert/debian/copyright
new file mode 100644
index 0000000..ce04a83
--- /dev/null
+++ b/devcert/debian/copyright
@@ -0,0 +1,23 @@
+Format: http://svn.debian.org/wsvn/dep/web/deps/dep5.mdwn?op=file&rev=173
+Upstream-Name: gfiber-device-cert
+Upstream-Contact: Avery Pennarun <apenwarr@google.com>
+Source: https://gfiber.googlesource.com/vendor/google/platform/+/master/devcert/
+
+Files: *
+Copyright: © 2015 Avery Pennarun <apenwarr@google.com>
+License: Apache
+ *
+ * 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.
+ *
diff --git a/devcert/debian/gen-changelog b/devcert/debian/gen-changelog
new file mode 120000
index 0000000..240869a
--- /dev/null
+++ b/devcert/debian/gen-changelog
@@ -0,0 +1 @@
+../../logupload/client/debian/gen-changelog
\ No newline at end of file
diff --git a/devcert/debian/install b/devcert/debian/install
new file mode 100644
index 0000000..9b5b49d
--- /dev/null
+++ b/devcert/debian/install
@@ -0,0 +1,2 @@
+gfiber-device-cert                     /usr/bin
+openssl.conf                           /usr/share/gfiber-device-cert
diff --git a/devcert/debian/postinst b/devcert/debian/postinst
new file mode 100755
index 0000000..20dbd92
--- /dev/null
+++ b/devcert/debian/postinst
@@ -0,0 +1,3 @@
+#DEBHELPER#
+
+gfiber-device-cert
diff --git a/devcert/debian/rules b/devcert/debian/rules
new file mode 100755
index 0000000..02843c4
--- /dev/null
+++ b/devcert/debian/rules
@@ -0,0 +1,13 @@
+#!/usr/bin/make -f
+
+override_dh_auto_test:
+override_dh_auto_install:
+
+%:
+	dh $@
+
+binary: debian/changelog
+
+.PHONY: debian/changelog
+debian/changelog:
+	debian/gen-changelog >$@
diff --git a/devcert/gfiber-device-cert b/devcert/gfiber-device-cert
new file mode 100755
index 0000000..10e259a
--- /dev/null
+++ b/devcert/gfiber-device-cert
@@ -0,0 +1,60 @@
+#!/bin/sh
+[ -n "$KEYFILE" ] || KEYFILE=/etc/ssl/private/device.key
+[ -n "$CERTFILE" ] || CERTFILE=/etc/ssl/certs/device.pem
+[ -n "$SSLCONF" ] || SSLCONF=/usr/share/gfiber-device-cert/openssl.conf
+
+
+fatal() {
+  echo "$0:" "$@" >&2
+  exit 99
+}
+
+
+host=$(
+  hostname -f |
+    sed -e 's/\([^.]*\.[^.]*\).*/\1/' \
+        -e 's/\./_/g'
+)
+full_cn="f88fca-Linux-$host"
+
+if [ "$1" = "-f" ] || [ ! -e "$CERTFILE" ]; then
+  echo "Generating cert for '$full_cn'" >&2
+  touch "$CERTFILE.new" 2>/dev/null ||
+    fatal "can't write '$CERTFILE.new'.  run as root?"
+  touch "$KEYFILE.new" 2>/dev/null ||
+    fatal "can't write '$KEYFILE.new'.  run as root?"
+  openssl req \
+      -config "$SSLCONF" \
+      -batch \
+      -new \
+      -x509 \
+      -newkey rsa:2048 \
+      -nodes \
+      -keyout "$KEYFILE.new" \
+      -out "$CERTFILE.new" \
+      -days 36500 \
+      -subj "/CN=$full_cn" || fatal "failed to generate key"
+  chmod a+r "$CERTFILE.new"
+  mv "$KEYFILE.new" "$KEYFILE"
+  mv "$CERTFILE.new" "$CERTFILE"
+fi
+
+[ -r "$CERTFILE" ] || fatal "'$CERTFILE' is not readable. run as root?"
+
+real_cn=$(
+  openssl x509 -in "$CERTFILE" -text -noout |
+  perl -ne '/Subject: CN=(.*)/ && { print $1 }'
+)
+
+openssl x509 -in "$CERTFILE" -text || fatal "error dumping certificate"
+
+if [ "$real_cn" != "$full_cn" ]; then
+  echo >&2
+  echo "Warning: expected 'CN=$full_cn'" >&2
+  echo "              got 'CN=$real_cn'" >&2
+  echo "Warning: this cert might not be accepted by servers." >&2
+  echo "Warning: to regenerate it, run: $0 -f" >&2
+  exit 10
+fi
+
+exit 0
diff --git a/devcert/gfiber-device-cert.test b/devcert/gfiber-device-cert.test
new file mode 100755
index 0000000..33f29d3
--- /dev/null
+++ b/devcert/gfiber-device-cert.test
@@ -0,0 +1,25 @@
+#!/bin/sh
+. ./wvtest/wvtest.sh
+
+WVSTART "gfiber-device-cert"
+
+export CERTFILE=cert.tmp KEYFILE=key.tmp SSLCONF=openssl.conf
+
+# Generate an initial certificate
+rm -f "$CERTFILE" "$KEYFILE"
+WVFAIL [ -r "$CERTFILE" ]
+WVFAIL [ -r "$KEYFILE" ]
+WVPASS ./gfiber-device-cert
+WVPASS [ -r "$CERTFILE" ]
+WVPASS [ -r "$KEYFILE" ]
+sum1=$(sha1sum "$CERTFILE" "$KEYFILE")
+
+# should not replace the cert
+WVPASS ./gfiber-device-cert
+sum2=$(sha1sum "$CERTFILE" "$KEYFILE")
+WVPASSEQ "$sum1" "$sum2"
+
+# -f should cause the cert to be replaced
+WVPASS ./gfiber-device-cert -f
+sum2=$(sha1sum "$CERTFILE" "$KEYFILE")
+WVPASSNE "$sum1" "$sum2"
diff --git a/devcert/openssl.conf b/devcert/openssl.conf
new file mode 100644
index 0000000..a11a653
--- /dev/null
+++ b/devcert/openssl.conf
@@ -0,0 +1,5 @@
+
+[req]
+distinguished_name = req_distinguished_name
+
+[req_distinguished_name]
diff --git a/devcert/wvtest b/devcert/wvtest
new file mode 120000
index 0000000..75927a5
--- /dev/null
+++ b/devcert/wvtest
@@ -0,0 +1 @@
+../cmds/wvtest
\ No newline at end of file
diff --git a/diags/common/Makefile b/diags/common/Makefile
index 1d19579..cf9801b 100644
--- a/diags/common/Makefile
+++ b/diags/common/Makefile
@@ -1,7 +1,5 @@
 default:	all
 
-BINARY = common_diags
-
 TARGETS=$(BINARY)
 INSTALL=install
 PREFIX=$(DESTDIR)/usr
@@ -12,56 +10,27 @@
 CC=$(CROSS_COMPILE)gcc
 CXX=$(CROSS_COMPILE)g++
 RM=rm -f
-CFLAGS = -Wall -Wimplicit -Wno-unknown-pragmas -W -std=c99 -D_GNU_SOURCE
+CFLAGS = -Wall -Werror -Wimplicit -Wno-unknown-pragmas -D_GNU_SOURCE
 
 CFLAGS += $(EXTRA_CFLAGS)
 LDFLAGS += $(EXTRA_LDFLAGS)
 
-# enable the platform we're supporting
-ifeq ($(BR2_PACKAGE_BCM_NEXUS),y)
-  CFLAGS += -DBROADCOM
-  NOSTUB=1
-endif
-ifeq ($(BR2_PACKAGE_MINDSPEED_DRIVERS),y)
-  CFLAGS += -DMINDSPEED
-  NOSTUB=1
-endif
-
-ifeq ($(BR2_TARGET_GOOGLE_PLATFORM),gfiberlt)
-  CFLAGS += -DGFIBER_LT
-  NOSTUB=1
-endif
-
-ifndef NOSTUB
-  CFLAGS += -DSTUB
-  LDFLAGS += -lm
-endif
-
 CFLAGS += -g
 
 IFLAGS += $(patsubst %,-I%,$(INC_DIRS))
-
-CFILES = $(wildcard *.c ../common/*.c)
+CFILES = $(wildcard *.c)
 OFILES = $(patsubst %.c,%.o,$(CFILES))
 
-ifndef BRUNO_ARCH
-$(warning BRUNO_ARCH is undefined.  Set it to arm or mips or i386)
-endif
-
 all:	$(OFILES)
 
 install:
 
-
 install-libs:
 	@:
 
 test:
 	@:
 
-$(BINARY):	$(OFILES)
-	$(CC) $^ $(LDFLAGS) -o $@
-
 %.o:	%.c
 	$(CC) $(CFLAGS) $(IFLAGS) -c $^ -c
 
diff --git a/diags/spacecast/Makefile b/diags/spacecast/Makefile
index aba5353..24f7c7b 100644
--- a/diags/spacecast/Makefile
+++ b/diags/spacecast/Makefile
@@ -12,42 +12,15 @@
 CC=$(CROSS_COMPILE)gcc
 CXX=$(CROSS_COMPILE)g++
 RM=rm -f
-CFLAGS = -Wall -Wimplicit -Wno-unknown-pragmas -W -std=c99 -D_GNU_SOURCE
 
+CFLAGS = -Wall -Werror -Wimplicit -Wno-unknown-pragmas -D_GNU_SOURCE -g
 CFLAGS += $(EXTRA_CFLAGS)
 LDFLAGS += $(EXTRA_LDFLAGS)
 
-# enable the platform we're supporting
-ifeq ($(BR2_PACKAGE_BCM_NEXUS),y)
-  CFLAGS += -DBROADCOM
-  NOSTUB=1
-endif
-ifeq ($(BR2_PACKAGE_MINDSPEED_DRIVERS),y)
-  CFLAGS += -DMINDSPEED
-  NOSTUB=1
-endif
-
-ifeq ($(BR2_TARGET_GOOGLE_PLATFORM),gfiberlt)
-  CFLAGS += -DGFIBER_LT
-  NOSTUB=1
-endif
-
-ifndef NOSTUB
-  CFLAGS += -DSTUB
-  LDFLAGS += -lm
-endif
-
-CFLAGS += -g
-
 IFLAGS += $(patsubst %,-I%,$(INC_DIRS))
-
 CFILES = $(wildcard *.c ../common/*.c)
 OFILES = $(patsubst %.c,%.o,$(CFILES))
 
-ifndef BRUNO_ARCH
-$(warning BRUNO_ARCH is undefined.  Set it to arm or mips or i386)
-endif
-
 all:	$(TARGETS)
 
 install:
diff --git a/diags/spacecast/ge_test.c b/diags/spacecast/ge_test.c
index 8a4b0bf..4ec8c7c 100644
--- a/diags/spacecast/ge_test.c
+++ b/diags/spacecast/ge_test.c
@@ -971,7 +971,7 @@
 
 /* This is for Marvell 88E1512 only */
 int lan_lpbk(int argc, char *argv[]) {
-  int reg, data;
+  int data;
   bool loopback_on = false;
 
   if (argc != 2) {
diff --git a/diags/windcharger/Makefile b/diags/windcharger/Makefile
index 748b6d4..e1f40ed 100644
--- a/diags/windcharger/Makefile
+++ b/diags/windcharger/Makefile
@@ -12,42 +12,15 @@
 CC=$(CROSS_COMPILE)gcc
 CXX=$(CROSS_COMPILE)g++
 RM=rm -f
-CFLAGS = -Wall -Wimplicit -Wno-unknown-pragmas -W -std=c99 -D_GNU_SOURCE
 
+CFLAGS = -Wall -Werror -Wimplicit -Wno-unknown-pragmas -D_GNU_SOURC -g
 CFLAGS += $(EXTRA_CFLAGS)
 LDFLAGS += $(EXTRA_LDFLAGS)
 
-# enable the platform we're supporting
-ifeq ($(BR2_PACKAGE_BCM_NEXUS),y)
-  CFLAGS += -DBROADCOM
-  NOSTUB=1
-endif
-ifeq ($(BR2_PACKAGE_MINDSPEED_DRIVERS),y)
-  CFLAGS += -DMINDSPEED
-  NOSTUB=1
-endif
-
-ifeq ($(BR2_TARGET_GOOGLE_PLATFORM),gfiberlt)
-  CFLAGS += -DGFIBER_LT
-  NOSTUB=1
-endif
-
-ifndef NOSTUB
-  CFLAGS += -DSTUB
-  LDFLAGS += -lm
-endif
-
-CFLAGS += -g
-
 IFLAGS += $(patsubst %,-I%,$(INC_DIRS))
-
 CFILES = $(wildcard ../common/*.c *.c)
 OFILES = $(patsubst %.c,%.o,$(CFILES))
 
-ifndef BRUNO_ARCH
-$(warning BRUNO_ARCH is undefined.  Set it to arm or mips or i386)
-endif
-
 all:	$(TARGETS)
 
 install:
diff --git a/ginstall/Makefile b/ginstall/Makefile
index 84278af..2d63246 100644
--- a/ginstall/Makefile
+++ b/ginstall/Makefile
@@ -9,7 +9,7 @@
 install:
 	mkdir -p $(BINDIR)
 	cp ginstall.py $(BINDIR)
-	ln -sf ginstall.py $(BINDIR)/ginstall
+	ln -sf ginstall.py $(BINDIR)/ginstall.real
 
 install-libs:
 	@echo "No libs to install."
diff --git a/ginstall/ginstall.py b/ginstall/ginstall.py
index 8443209..c53f1af 100755
--- a/ginstall/ginstall.py
+++ b/ginstall/ginstall.py
@@ -50,7 +50,6 @@
 
 # Error codes.
 HNVRAM_ERR = 1
-GINSTALL_RUNNING_ERR = 2
 
 # unit tests can override these with fake versions
 BUFSIZE = 4 * 1024            # 64k causes b/14299411
@@ -59,14 +58,6 @@
 NANDDUMP = ['nanddump']
 SGDISK = 'sgdisk'
 
-# Lock prefix is used for process locking.  The full file will be saved with
-# ".lock" appended to the end.
-LOCK_PREFIX = '/tmp/ginstall'
-
-# Status file for informing a potential second ginstall that an installation has
-# already run to completion.
-GINSTALL_COMPLETED_FILE = '/tmp/ginstall_complete'
-
 F = {
     'ETCPLATFORM': '/etc/platform',
     'ETCVERSION': '/etc/version',
@@ -108,36 +99,6 @@
   pass
 
 
-class PidLock(object):
-  """A class meant to handle process mutual exclusion through a lock file.
-
-     Usage: create the lock, then use in a `with' statement.
-
-     ex:
-        with PidLock(prefix):
-          # Lock is now acquired.
-          ... do atomic things ...
-
-        # Lock is now released.
-        ... do other things ...
-  """
-
-  def __init__(self, lock_path):
-    self.lock_path = lock_path
-
-  def __enter__(self):
-    # Returns immediately if lockfile is already taken.
-    cmd = ['lockfile-create', '--use-pid', '--retry', '0', self.lock_path]
-    res = subprocess.call(cmd)
-    if res != 0:
-      raise LockException('Unable to acquire lock')
-    return self
-
-  def __exit__(self, *err):
-    cmd = ['lockfile-remove', self.lock_path]
-    subprocess.call(cmd)
-
-
 class Fatal(Exception):
   """An exception that we print as just an error, with no backtrace."""
   pass
@@ -962,35 +923,29 @@
   if not (opt.drm or opt.tar or opt.partition):
     o.fatal('Expected at least one of --partition, --tar, or --drm')
 
-  try:
-    with PidLock(LOCK_PREFIX):
-      quiet = opt.quiet
+  quiet = opt.quiet
 
-      if opt.basepath:
-        # Standalone test harness can pass in a fake root path.
-        AddBasePath(opt.basepath)
+  if opt.basepath:
+    # Standalone test harness can pass in a fake root path.
+    AddBasePath(opt.basepath)
 
-      if opt.drm:
-        WriteDrm(opt)
+  if opt.drm:
+    WriteDrm(opt)
 
-      if opt.tar and not opt.partition:
-        # default to the safe option if not given
-        opt.partition = 'other'
+  if opt.tar and not opt.partition:
+    # default to the safe option if not given
+    opt.partition = 'other'
 
-      partition = GetPartition(opt)
-      if opt.tar:
-        f = OpenPathOrUrl(opt.tar)
-        InstallImage(f, partition, skiploader=opt.skiploader,
-                     skiploadersig=opt.skiploadersig)
+  partition = GetPartition(opt)
+  if opt.tar:
+    f = OpenPathOrUrl(opt.tar)
+    InstallImage(f, partition, skiploader=opt.skiploader,
+                 skiploadersig=opt.skiploadersig)
 
-      if partition is not None and SetBootPartition(partition) != 0:
-        VerbosePrint('Unable to set boot partition\n')
-        return HNVRAM_ERR
+  if partition is not None and SetBootPartition(partition) != 0:
+    VerbosePrint('Unable to set boot partition\n')
+    return HNVRAM_ERR
 
-      open(GINSTALL_COMPLETED_FILE, 'w').close()
-  except LockException:
-    VerbosePrint('Another instance of ginstall is running\n')
-    return GINSTALL_RUNNING_ERR
   return 0
 
 
diff --git a/ginstall/ginstall_test.py b/ginstall/ginstall_test.py
index 9c6b945..928cf58 100755
--- a/ginstall/ginstall_test.py
+++ b/ginstall/ginstall_test.py
@@ -112,18 +112,6 @@
     self.assertFalse(ginstall.IsIdentical(
         'loader', loader, open('testdata/img/loader1.bin')))
 
-  def testLockExceptions(self):
-    lock_prefix = '/tmp/ginstall_test_lock'
-    lock = ginstall.PidLock(lock_prefix)
-    def LockFailure():
-      with lock:
-        with ginstall.PidLock(lock_prefix):
-          pass
-    self.assertRaises(ginstall.LockException, LockFailure)
-    # Asserts no exceptions happen during normal usage.
-    with lock:
-      pass
-
   def testIsMtdNand(self):
     mtd = ginstall.F['MTD_PREFIX']
     self.assertFalse(ginstall.IsMtdNand(mtd + '6'))
diff --git a/logupload/client/.gitignore b/logupload/client/.gitignore
index 7ba05e9..6f2ca86 100644
--- a/logupload/client/.gitignore
+++ b/logupload/client/.gitignore
@@ -1 +1,2 @@
 upload-crash-log2
+upload-logs
diff --git a/logupload/client/Makefile b/logupload/client/Makefile
index 8b51206..f7a56d3 100644
--- a/logupload/client/Makefile
+++ b/logupload/client/Makefile
@@ -1,34 +1,51 @@
+default: all
+
 CC:=$(CROSS_COMPILE)gcc
-CPP:=$(CROSS_COMPILE)g++
+CXX:=$(CROSS_COMPILE)g++
 LD:=$(CROSS_COMPILE)ld
 AR:=$(CROSS_COMPILE)ar
 RANLIB:=$(CROSS_COMPILE)ranlib
 STRIP:=$(CROSS_COMPILE)strip
 USRBINDIR=$(DESTDIR)/usr/bin
 
-CFLAGS += -Wall -Werror $(EXTRACFLAGS)
-LDFLAGS += -lrt -lcurl -lz -lm $(EXTRALDFLAGS)
+CFLAGS+=-Wall -Werror $(EXTRACFLAGS)
+LDFLAGS+=$(EXTRALDFLAGS)
+LIBS=-lrt -lcurl -lz -lm
 
 # Test Flags
-TEST_LDFLAGS=$(LDFLAGS) -lgtest -pthread
+TEST_LDFLAGS=$(LDFLAGS)
+TEST_LIBS=$(LIBS) -lgtest -pthread
 
-all: upload-crash-log2
-tests: kvextract_test
-SRCS = log_uploader.c kvextract.c upload.c utils.c
+OBJS = log_uploader.o kvextract.o upload.o utils.o
 INCS = kvextract.h upload.h utils.h
-MAIN_SRC = log_uploader_main.c
+MAIN_OBJ = log_uploader_main.o
+TESTS = kvextract_test utils_test log_uploader_test
 
-upload-crash-log2: $(SRCS) $(INCS) $(MAIN_SRC)
-	$(CC) $(CFLAGS) $(SRCS) $(MAIN_SRC) -o $@ $(LDFLAGS)
+all: upload-crash-log2 upload-logs
 
-kvextract_test: $(SRCS) $(INCS) kvextract_test.c
-	$(CPP) $(CFLAGS) kvextract_test.c $(SRCS) -o $@ $(TEST_LDFLAGS)
+tests: all $(TESTS)
 
-utils_test: $(SRCS) $(INCS) utils_test.c
-	$(CPP) $(CFLAGS) utils_test.c $(SRCS) -o $@ $(TEST_LDFLAGS)
+kvextract_test: kvextract_test.o
+utils_test: utils_test.o
+log_uploader_test: log_uploader_test.o
 
-log_uploader_test: $(SRCS) $(INCS) log_uploader_test.c
-	$(CPP) $(CFLAGS) log_uploader_test.c $(SRCS) -o $@ $(TEST_LDFLAGS)
+# an alias the Debian package can use, since upload-crash-log2 is kind of
+# a weird name, present mainly for backward compatibility with the old
+# loguploader on GFiber CPE devices (for now).
+upload-logs: upload-crash-log2
+	ln -f $< $@
+
+%.o: %.c $(INCS)
+	$(CC) $(CFLAGS) -c -o $@ $<
+
+%.o: %.cc $(INCS)
+	$(CXX) $(CFLAGS) -c -o $@ $<
+
+upload-crash-log2: $(OBJS) $(MAIN_OBJ)
+	$(CC) $(LDFLAGS) -o $@ $^ $(LIBS)
+
+%_test: %_test.o $(OBJS)
+	$(CXX) $(TEST_LDFLAGS) -o $@ $^ $(TEST_LIBS)
 
 install:
 	mkdir -p $(USRBINDIR)
@@ -37,10 +54,14 @@
 install-libs:
 	@echo "No libs to install."
 
-test: kvextract_test utils_test log_uploader_test
-	./kvextract_test
-	./utils_test
-	./log_uploader_test
+test: tests
+	set -e; for d in $(TESTS); do ./$$d; done
+	# Ensure proper handling of binary files
+	rm -f test.tmp
+	./upload-crash-log2 --stdin=test --stdout <upload-crash-log2 >test.tmp
+	cmp upload-crash-log2 test.tmp
+	rm -f test.tmp
+	wvtest/wvtestrun ./prefix-logs.test
 
 clean:
-	rm -f upload-crash-log2 *.o
+	rm -f upload-crash-log2 upload-logs $(TESTS) *~ *.o
diff --git a/logupload/client/debian/.gitignore b/logupload/client/debian/.gitignore
new file mode 100644
index 0000000..e0137c3
--- /dev/null
+++ b/logupload/client/debian/.gitignore
@@ -0,0 +1,6 @@
+/files
+/changelog
+/*.debhelper
+/gfiber-loguploader.debhelper.log
+/gfiber-loguploader.substvars
+/gfiber-loguploader
diff --git a/logupload/client/debian/70-gfiber-loguploader.conf b/logupload/client/debian/70-gfiber-loguploader.conf
new file mode 100644
index 0000000..8c63b39
--- /dev/null
+++ b/logupload/client/debian/70-gfiber-loguploader.conf
@@ -0,0 +1,3 @@
+# rsyslog config for writing to gfiber-loguploader named pipe.
+
+*.* |/var/log/gfiber-loguploader/pipe
diff --git a/logupload/client/debian/README.Debian b/logupload/client/debian/README.Debian
new file mode 100644
index 0000000..2cf7e88
--- /dev/null
+++ b/logupload/client/debian/README.Debian
@@ -0,0 +1,5 @@
+GFiber loguploader
+------------------
+
+Find the source here:
+https://gfiber.googlesource.com/vendor/google/platform/+/master/logupload/client/
diff --git a/logupload/client/debian/clean b/logupload/client/debian/clean
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/logupload/client/debian/clean
diff --git a/logupload/client/debian/compat b/logupload/client/debian/compat
new file mode 100644
index 0000000..7f8f011
--- /dev/null
+++ b/logupload/client/debian/compat
@@ -0,0 +1 @@
+7
diff --git a/logupload/client/debian/control b/logupload/client/debian/control
new file mode 100644
index 0000000..b7455d4
--- /dev/null
+++ b/logupload/client/debian/control
@@ -0,0 +1,14 @@
+Source: gfiber-loguploader
+Section: net
+Priority: extra
+Maintainer: Avery Pennarun <apenwarr@google.com>
+Build-Depends: debhelper (>= 7.0.50~)
+Standards-Version: 3.9.2
+Homepage: https://gfiber.googlesource.com/vendor/google/platform/+/master/logupload/client/
+Vcs-Git: https://gfiber.googlesource.com/vendor/google/platform
+Vcs-Browser: https://gfiber.googlesource.com/vendor/google/platform
+
+Package: gfiber-loguploader
+Architecture: any
+Depends: rsyslog (>= 5.8), gfiber-device-cert (>= 0.0)
+Description: Tool for uploading syslogs to GFiber log servers.
diff --git a/logupload/client/debian/copyright b/logupload/client/debian/copyright
new file mode 100644
index 0000000..d2bdd00
--- /dev/null
+++ b/logupload/client/debian/copyright
@@ -0,0 +1,23 @@
+Format: http://svn.debian.org/wsvn/dep/web/deps/dep5.mdwn?op=file&rev=173
+Upstream-Name: gfiber-loguploader
+Upstream-Contact: Avery Pennarun <apenwarr@google.com>
+Source: https://gfiber.googlesource.com/vendor/google/platform/+/master/logupload/client/
+
+Files: *
+Copyright: © 2015 Jeffrey Kardatzke <jkardatzke@google.com>
+License: Apache
+ *
+ * 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.
+ *
diff --git a/logupload/client/debian/dirs b/logupload/client/debian/dirs
new file mode 100644
index 0000000..7f3dd46
--- /dev/null
+++ b/logupload/client/debian/dirs
@@ -0,0 +1 @@
+/var/log/gfiber-loguploader
diff --git a/logupload/client/debian/gen-changelog b/logupload/client/debian/gen-changelog
new file mode 100755
index 0000000..c5c830d
--- /dev/null
+++ b/logupload/client/debian/gen-changelog
@@ -0,0 +1,17 @@
+#!/bin/sh
+read junk pkgname <debian/control
+git log --pretty='format:'"$pkgname"' (SHA:%H) unstable; urgency=low
+
+  * %s
+  
+ -- %aN <%aE>  %aD
+' . |
+python -Sc '
+import os, re, subprocess, sys
+
+def Describe(g):
+  s = subprocess.check_output(["git", "describe", "--", g.group(1)]).strip()
+  return re.sub(r"^\D*", "", s)
+
+print re.sub(r"SHA:([0-9a-f]+)", Describe, sys.stdin.read())
+'
diff --git a/logupload/client/debian/init b/logupload/client/debian/init
new file mode 100755
index 0000000..593270e
--- /dev/null
+++ b/logupload/client/debian/init
@@ -0,0 +1,68 @@
+#!/bin/sh
+#
+### BEGIN INIT INFO
+# Provides:          gfiber-loguploader
+# Required-Start:    $syslog $remote_fs
+# Required-Stop:     $syslog $remote_fs
+# Default-Start:     2 3 4 5
+# Default-Stop:      0 1 6
+# Short-Description: GFiber loguploader client
+### END INIT INFO
+
+[ -x /usr/bin/upload-logs ] || exit 0
+. /lib/lsb/init-functions
+
+atomic_stdin() {
+  rm -f "$1" "$1.new"
+  cat >"$1.new"
+  mv "$1.new" "$1"
+}
+
+case "$1" in
+  start)
+    log_daemon_msg "Starting GFiber log uploader" "upload-logs"
+    if ! [ -e /etc/ssl/certs/device.pem -a \
+           -e /etc/ssl/private/device.key ]; then
+      echo >&2
+      echo "Cert missing: /etc/ssl/certs/device.pem and /etc/ssl/private/device.key." >&2
+      echo >&2
+      log_end_msg 1
+      exit 1
+    fi
+    rm -f /tmp/ssl
+    ln -sf /etc/ssl/. /tmp/ssl
+    uname -r | sed 's/ /-/g' | atomic_stdin /etc/version
+    uname | atomic_stdin /tmp/platform
+    # Our hostnames are something like hostname.cluster.whatever.com.
+    # We want the hostname.cluster part to be part of the certname, but
+    # sadly the name field in our certs isn't super happy about that, so
+    # let's use _ instead of dot, where relevant.
+    hostname -f |
+      sed -e 's/\.googlefiber\.net$//' \
+          -e 's/\.nyc\.corp\.google\.com$//' \
+          -e 's/\./_/g' |
+      atomic_stdin /tmp/serial
+    cd /
+    upload-logs-loop </dev/null &
+    log_end_msg 0
+    exit 0
+    ;;
+  stop)
+    log_daemon_msg "Stopping GFiber log uploader" "upload-logs"
+    pkill upload-logs-loop
+    pkill upload-logs
+    pkill -f prefix-logs
+    log_end_msg 0
+    exit 0
+    ;;
+  reload|force-reload|restart)
+    $0 stop; $0 start
+    ;;
+  status)
+    pgrep upload-logs >/dev/null
+    exit $?
+    ;;
+  *)
+    echo "Usage: $0 {start|stop|restart|reload|force-reload|status}"
+    exit 1
+esac
diff --git a/logupload/client/debian/install b/logupload/client/debian/install
new file mode 100644
index 0000000..2f83d49
--- /dev/null
+++ b/logupload/client/debian/install
@@ -0,0 +1,4 @@
+upload-logs                            /usr/bin
+prefix-logs                            /usr/bin
+debian/upload-logs-loop                /usr/bin
+debian/70-gfiber-loguploader.conf      /etc/rsyslog.d
diff --git a/logupload/client/debian/postinst b/logupload/client/debian/postinst
new file mode 100644
index 0000000..bca62bc
--- /dev/null
+++ b/logupload/client/debian/postinst
@@ -0,0 +1,9 @@
+#DEBHELPER#
+
+umask 007
+PNAME=/var/log/gfiber-loguploader/pipe
+if ! [ -p "$PNAME" ]; then
+  rm -f "$PNAME"
+  mkfifo "$PNAME"
+fi
+service rsyslog restart
diff --git a/logupload/client/debian/rules b/logupload/client/debian/rules
new file mode 100755
index 0000000..02843c4
--- /dev/null
+++ b/logupload/client/debian/rules
@@ -0,0 +1,13 @@
+#!/usr/bin/make -f
+
+override_dh_auto_test:
+override_dh_auto_install:
+
+%:
+	dh $@
+
+binary: debian/changelog
+
+.PHONY: debian/changelog
+debian/changelog:
+	debian/gen-changelog >$@
diff --git a/logupload/client/debian/upload-logs-loop b/logupload/client/debian/upload-logs-loop
new file mode 100755
index 0000000..2f657f2
--- /dev/null
+++ b/logupload/client/debian/upload-logs-loop
@@ -0,0 +1,7 @@
+#!/bin/sh
+while :; do
+  # TODO(apenwarr): register a new logtype and change the --logtype here.
+  prefix-logs </var/log/gfiber-loguploader/pipe |
+    upload-logs --logtype server --freq 60 --stdin dmesg
+  sleep 1
+done 2>&1 | logger -t upload-logs-loop
diff --git a/logupload/client/kvextract.c b/logupload/client/kvextract.c
index 9d0c426..bcfd84a 100644
--- a/logupload/client/kvextract.c
+++ b/logupload/client/kvextract.c
@@ -34,6 +34,7 @@
   len = read_file_as_string(filepath, (*pair)->value, sizeof((*pair)->value));
   if (len < 0) {
     free(*pair);
+    *pair = NULL;
     return KV_NOTHING;
   }
   rstrip((*pair)->value);
@@ -90,7 +91,7 @@
               sizeof(struct sockaddr_in6), pair_tail->value,
               sizeof(pair_tail->value), NULL, 0, NI_NUMERICHOST);
           if (rv != 0) {
-            fprintf(stderr, "getnameinfo() failed: %s\n", gai_strerror(rv));
+            fprintf(stderr, "getnameinfo(): %s\n", gai_strerror(rv));
             free_kv_pairs(pair_head);
             return NULL;
           }
diff --git a/logupload/client/kvextract.h b/logupload/client/kvextract.h
index a6df5af..3c5bb56 100644
--- a/logupload/client/kvextract.h
+++ b/logupload/client/kvextract.h
@@ -1,6 +1,10 @@
 #ifndef _H_LOGUPLOAD_CLIENT_KVEXTRACT_H_
 #define _H_LOGUPLOAD_CLIENT_KVEXTRACT_H_
 
+#ifdef __cplusplus
+extern "C" {
+#endif
+
 #include <sys/socket.h>
 #include <ifaddrs.h>
 
@@ -44,4 +48,8 @@
 struct kvpair* extract_kv_pairs(struct kvextractparams* params);
 void free_kv_pairs(struct kvpair* pairs);
 
+#ifdef __cplusplus
+}
+#endif
+
 #endif  // _H_LOGUPLOAD_CLIENT_KVEXTRACT_H_
diff --git a/logupload/client/kvextract_test.c b/logupload/client/kvextract_test.cc
similarity index 100%
rename from logupload/client/kvextract_test.c
rename to logupload/client/kvextract_test.cc
diff --git a/logupload/client/log_uploader.c b/logupload/client/log_uploader.c
index 983bfb8..5b63174 100644
--- a/logupload/client/log_uploader.c
+++ b/logupload/client/log_uploader.c
@@ -17,8 +17,7 @@
   struct tm timeinfo;
   struct timespec curr_time;
   if (read_file_as_string(version_path, version, sizeof(version)) < 0) {
-    fprintf(stderr, "failed reading %s\n", version_path);
-    return -1;
+    strcpy(version, "unknown-version");
   }
   rstrip(version);
   memset(&timeinfo, 0, sizeof(timeinfo));
@@ -30,7 +29,12 @@
       version, (int)curr_time.tv_sec, (int)(curr_time.tv_nsec/1000000L),
       timeinfo.tm_mon + 1, timeinfo.tm_mday, timeinfo.tm_hour, timeinfo.tm_min,
       timeinfo.tm_sec, path_exists(ntp_sync_path));
-  return write_to_file(output_path, buf) < 0;
+  int rv = write_to_file(output_path, buf);
+  if (rv < 0) {
+    perror(output_path);
+    return -1;
+  }
+  return 0;
 }
 
 char* parse_and_consume_log_data(struct log_parse_params* params) {
@@ -69,11 +73,11 @@
         if (!wrote_start_marker) {
           // Write out the start marker.
           if (write_to_file(params->dev_kmsg_path, LOG_MARKER_START_LINE) < 0) {
-            fprintf(stderr, "failed to write out start marker\n");
+            perror("start marker");
             return NULL;
           }
           if (logmark_once(params->dev_kmsg_path, params->version_path,
-                params->ntp_synced_path)) {
+                params->ntp_synced_path) < 0) {
             fprintf(stderr, "failed to execute logmark-once properly\n");
             return NULL;
           }
@@ -99,7 +103,7 @@
         perror("kernel memory possibly corrupted, devkmsg_read");
         continue;
       } else {
-        fprintf(stderr, "failed reading from /dev/kmsg: %s\n", strerror(errno));
+        perror("/dev/kmsg");
         return NULL;
       }
     }
diff --git a/logupload/client/log_uploader.h b/logupload/client/log_uploader.h
index 75fcac5..bb6011c 100644
--- a/logupload/client/log_uploader.h
+++ b/logupload/client/log_uploader.h
@@ -1,6 +1,10 @@
 #ifndef _H_LOGUPLOAD_CLIENT_LOG_UPLOADER_H_
 #define _H_LOGUPLOAD_CLIENT_LOG_UPLOADER_H_
 
+#ifdef __cplusplus
+extern "C" {
+#endif
+
 #include "kvextract.h"
 
 #define LOG_MARKER_START "*LOG_UPLOAD_START*"
@@ -14,6 +18,7 @@
   int upload_all;
   int use_stdout;
   int use_stdin;
+  int freq;
   char upload_target[1024];
 };
 
@@ -44,4 +49,8 @@
 int logmark_once(const char* output_path, const char* version_path,
     const char* ntp_sync_path);
 
+#ifdef __cplusplus
+}
+#endif
+
 #endif  // _H_LOGUPLOAD_CLIENT_LOG_UPLOADER_H_
diff --git a/logupload/client/log_uploader_main.c b/logupload/client/log_uploader_main.c
index 1dc925a..8d3afc4 100644
--- a/logupload/client/log_uploader_main.c
+++ b/logupload/client/log_uploader_main.c
@@ -26,12 +26,13 @@
 #define COUNTER_MARKER_FILE "/tmp/loguploadcounter"
 #define LOGS_UPLOADED_MARKER_FILE "/tmp/logs-uploaded"
 #define DEFAULT_UPLOAD_TARGET "dmesg"
-#define MAX_LOG_SIZE 8*1024*1024 // 8 MB
+#define MAX_LOG_SIZE 8*1024*1024  // max bytes to upload in one run
+#define LOG_BUF_EXTRA 65536       // extra bytes for extra compression slop
 #define DEV_KMSG_PATH "/dev/kmsg"
 #define NTP_SYNCED_PATH "/tmp/ntp.synced"
 #define VERSION_PATH "/etc/version"
-#define SERIAL_PATH "/etc/serial"
-#define PLATFORM_PATH "/etc/platform"
+#define SERIAL_PATH "/tmp/serial"
+#define PLATFORM_PATH "/tmp/platform"
 // 8192 is the size of the buffer used in printk.c to store a line read from
 // /dev/kmsg before copying it into our userspace buffer
 #define LOG_LINE_BUFFER_SIZE 8192
@@ -45,6 +46,12 @@
 static int num_interfaces = sizeof(interfaces_to_check) /
   sizeof(interfaces_to_check[0]);
 
+volatile static int interrupted = 0;
+
+static void got_alarm(int sig) {
+  interrupted = 1;
+}
+
 // To allow overriding for testing.
 int getnameinfo_resolver(const struct sockaddr* sa, socklen_t salen, char* host,
     size_t hostlen, char* serv, size_t servlen, int flags) {
@@ -81,13 +88,15 @@
 }
 
 static void usage(const char* progname) {
-  fprintf(stderr, "Usage for: %s\n", progname);
-  fprintf(stderr, " --server URL Server URL (default: " DEFAULT_SERVER ")\n");
-  fprintf(stderr, " --all        Upload entire logs, not just new data\n");
-  fprintf(stderr, " --stdout     Print to stdout instead of uploading\n");
-  fprintf(stderr, " --stdin name Get data from stdin rather than /dev/kmsg and"
-      " upload to 'name' facility rather than 'dmesg', also disables "
-      "looping\n");
+  fprintf(stderr, "\nUsage: %s [options...]\n", progname);
+  fprintf(stderr, " --server URL    Server URL [" DEFAULT_SERVER "]\n");
+  fprintf(stderr, " --all           Upload entire logs, not just new data\n");
+  fprintf(stderr, " --logtype TYPE  Tell server which log category this is\n");
+  fprintf(stderr, " --freq SECS     Upload logs every SECS seconds [60]\n");
+  fprintf(stderr, " --stdout        Print to stdout instead of uploading\n");
+  fprintf(stderr,
+          " --stdin NAME    Get data from stdin, not /dev/kmsg, and\n"
+          "                   name uploaded file NAME rather than 'dmesg'\n");
   exit(EXIT_SUCCESS);
 }
 
@@ -97,9 +106,10 @@
   static struct option long_options[] = {
     { "server", required_argument, 0, 's' },
     { "all", no_argument, 0, 'a' },
+    { "logtype", required_argument, 0, 'l' },
+    { "freq", required_argument, 0, 'f' },
     { "stdout", no_argument, 0, 'd' },
     { "stdin", required_argument, 0, 'i' },
-    { "logtype", required_argument, 0 , 'l' },
     { 0, 0, 0, 0}
   };
 
@@ -126,8 +136,15 @@
       case 'l':
         snprintf(config->logtype, sizeof(config->logtype), "%s", optarg);
         break;
-     default:
-       return -1;
+      case 'f':
+        config->freq = atoi(optarg);
+        if (config->freq < 0) {
+          fprintf(stderr, "fatal: freq must be >= 0\n");
+          return -1;
+        }
+        break;
+      default:
+        return -1;
     }
   }
   if (optind < argc)
@@ -135,6 +152,14 @@
   return 0;
 }
 
+static int pick_delay(struct upload_config* config) {
+  // Randomize the sleep time to be near the specified amount, +/- 1/12th.
+  // (1/12th is weird, but it means +/- 5 for 60 seconds, which is nice).
+  int variance = config->freq / 12;
+  return ((config->freq - variance) +
+          (random() % (variance * 2 + 1)));
+}
+
 int main(int argc, char* const argv[]) {
   setvbuf(stdout, (char *) NULL, _IOLBF, 0);
 
@@ -148,18 +173,22 @@
   if (argc > 1) {
     if (parse_args(&config, argc, argv) < 0) {
       usage(argv[0]);
-      return 1;
+      return 99;
     }
   }
 
-  // Initiliaze the random number generator
+  struct sigaction sa = {};
+  sa.sa_handler = got_alarm;
+  sigaction(SIGALRM, &sa, NULL);
+
+  // Initialize the random number generator
   srandom(getpid() ^ time(NULL));
 
-  // Allocate this once and re-use it very time
-  char* log_buffer = (char*) malloc(MAX_LOG_SIZE);
+  // Allocate this once and re-use it every time
+  char* log_buffer = (char*) malloc(MAX_LOG_SIZE + LOG_BUF_EXTRA);
   if (!log_buffer) {
     fprintf(stderr, "Failed to allocate log_buffer!\n");
-    return 1;
+    return 98;
   }
 
   int kmsg_read_fd = 0;
@@ -168,8 +197,7 @@
     // Use nonblocking mode so we know when we're consumed all the recent data.
     kmsg_read_fd = open("/dev/kmsg", O_RDONLY | O_NONBLOCK);
     if (kmsg_read_fd < 0) {
-      fprintf(stderr, "failed to open /dev/kmsg for reading: %s\n",
-          strerror(errno));
+      perror("/dev/kmsg");
       return 1;
     }
   }
@@ -202,15 +230,20 @@
       // Read in all the data from stdin
       int num_read;
       total_read = num_read = 0;
+      interrupted = 0;
+      alarm(pick_delay(&config));
       while ((num_read = read(STDIN_FILENO, log_buffer + total_read,
-              MAX_LOG_SIZE - total_read)) > 0) {
+              MAX_LOG_SIZE - total_read)) > 0 && !interrupted) {
         total_read += num_read;
       }
-      if (num_read < 0) {
-        fprintf(stderr, "failed reading from stdin: %s\n", strerror(errno));
-        return 1;
+      if (num_read < 0 && errno != EINTR) {
+        perror("stdin");
+        return 2;
       }
-      log_buffer[total_read] = '\0';
+      if (num_read == 0 && total_read == 0) {
+        fprintf(stderr, "stdin: end of input. done.\n");
+        return 0;
+      }
       log_data_to_use = log_buffer;
     } else {
       // Remove the marker file to indicate we've completed the upload process.
@@ -223,14 +256,14 @@
           logmark_once(DEV_KMSG_PATH, VERSION_PATH,
           NTP_SYNCED_PATH)) {
         fprintf(stderr, "failed to execute logmark-once properly\n");
-        return 1;
+        return 3;
       }
 
       parse_params.total_read = MAX_LOG_SIZE;
       log_data_to_use = parse_and_consume_log_data(&parse_params);
       if (!log_data_to_use) {
         fprintf(stderr, "failed with logs parsing, abort!\n");
-        return 1;
+        return 4;
       }
       total_read = parse_params.total_read;
     }
@@ -238,14 +271,15 @@
     // Now we've read all of the log data into our buffer, proceed
     // with uploading or outputting it.
 
+    fprintf(stderr, "uploading %lu bytes of logs.\n", total_read);
     if (config.use_stdout) {
-      // Just print the whole thing to stdout
-      printf("%s", log_data_to_use);
+      // Just print the whole thing to stdout.  Note: might be binary.
+      fwrite(log_data_to_use, total_read, 1, stdout);
     } else {
       struct ifaddrs* ifaddr;
       if (getifaddrs(&ifaddr)) {
-        fprintf(stderr, "failed calling getifaddrs: %s\n", strerror(errno));
-        return 1;
+        perror("getifaddrs");
+        return 5;
       }
 
       struct kvextractparams kvparams;
@@ -262,28 +296,30 @@
       freeifaddrs(ifaddr);
       if (!kvpairs) {
         fprintf(stderr, "failure extracting kv pairs, abort\n");
-        return 1;
+        return 6;
       }
 
       // Adjust this if we moved the pointer.
-      unsigned long compressed_size = MAX_LOG_SIZE -
+      unsigned long compressed_size = MAX_LOG_SIZE + LOG_BUF_EXTRA -
         (log_data_to_use - log_buffer);
       int comp_result = deflate_inplace(&zstrm, (unsigned char*)log_data_to_use,
           total_read, &compressed_size);
       if (comp_result != Z_OK) {
-        return 1;
+        fprintf(stderr, "fatal: deflate_inplace failed\n");
+        return 7;
       }
 
       int upload_res = upload_file(config.server, config.upload_target,
             log_data_to_use, compressed_size, kvpairs);
       free_kv_pairs(kvpairs);
       if (upload_res) {
-        return 1;
+        fprintf(stderr, "upload_file failed\n");
+        return 8;
       }
       if (write_file_as_uint64(COUNTER_MARKER_FILE,
             parse_params.last_log_counter)) {
         fprintf(stderr, "unable to write out last log counter\n");
-        return 1;
+        return 9;
       }
       // Write the marker file to indicate we finished the upload.
       int marker_fd = open(LOGS_UPLOADED_MARKER_FILE, O_CREAT | O_WRONLY,
@@ -292,18 +328,19 @@
         close(marker_fd);
     }
 
-    if (write_to_file(DEV_KMSG_PATH, LOG_MARKER_END_LINE) < 0) {
-      fprintf(stderr, "failed to write out end marker\n");
-      return 1;
+    if (!config.use_stdin) {
+      if (write_to_file(DEV_KMSG_PATH, LOG_MARKER_END_LINE) < 0) {
+        perror("end marker");
+        return 10;
+      }
     }
 
-    if (!config.use_stdin) {
-      // Randomize the sleep time between 55 and 65 seconds which matches
-      // what is done in the log-delay script.
-      int sleep_dur = 55 + (random() % 11);
-      sleep(sleep_dur);
-    } else
+    if (!config.freq) {
       break;
+    } else if (!config.use_stdin) {
+      // if using stdin, we want to read incrementally instead.
+      sleep(pick_delay(&config));
+    }
   }
   free(log_buffer);
   return 0;
diff --git a/logupload/client/log_uploader_test.c b/logupload/client/log_uploader_test.cc
similarity index 100%
rename from logupload/client/log_uploader_test.c
rename to logupload/client/log_uploader_test.cc
diff --git a/logupload/client/prefix-logs b/logupload/client/prefix-logs
new file mode 100755
index 0000000..5d7acd0
--- /dev/null
+++ b/logupload/client/prefix-logs
@@ -0,0 +1,42 @@
+#!/usr/bin/python -S
+"""Fix prefixes in rsyslog lines to make them look like /dev/kmsg."""
+import os
+import sys
+import time
+
+uptimef = open('/proc/uptime')
+versionfile = os.getenv('VERSIONFILE', '/etc/version')
+version = open(versionfile).read().strip().replace(' ', '-')
+
+
+def Log(service, msg):
+  uptimef.seek(0)
+  uptime = float(uptimef.read().split(' ', 1)[0])
+  sys.stdout.write('<7>[%13.6f] %s: %s' % (uptime, service, msg))
+
+
+def main():
+  last_mark = 0
+  while True:
+    line = sys.stdin.readline()
+    if not line: break
+    # sigh, different rsyslog installs log the date in a different format.
+    # for robustness, parse backwards from the colon.
+    # Input format:
+    #    this is a date hostname service[pid]: msg\n
+    pre, msg = line.split(': ', 1)
+    servicepid = pre.split(' ')[-1]
+    service = servicepid.split('[', 1)[0]
+    now = time.time()
+    if now - last_mark >= 30:
+      Log('T', '%s %d %s ntp=1\n' % (
+          version,
+          time.time(),
+          time.strftime('%m/%d %H:%M:%S', time.localtime())))
+      last_mark = now
+    Log(service, msg)
+    sys.stdout.flush()
+
+
+if __name__ == '__main__':
+  main()
diff --git a/logupload/client/prefix-logs.test b/logupload/client/prefix-logs.test
new file mode 100755
index 0000000..983edc8
--- /dev/null
+++ b/logupload/client/prefix-logs.test
@@ -0,0 +1,41 @@
+#!/bin/sh
+. ./wvtest/wvtest.sh
+
+export VERSIONFILE='./testdata/version'
+WVSTART "prefix-logs"
+
+contains() {
+  [ "$1" != "${1%$2*}" ]
+}
+
+WVPASS contains foible ib
+WVPASS contains 'big deal' ' '
+WVFAIL contains foible ' '
+
+echo "time stamp crap silly whatever hostname foo: blue message ☺
+2015-09-30T22:37:34.207817-04:00 whatever.xxx.corp.google.com CRON[1734]: pam_unix(cron:session): session closed for user root
+Sep 30 19:42:10 blob102 apenwarr: hello world
+" |
+./prefix-logs | {
+  # prefix-logs always prepends an initial T: line as well as printing
+  # additional ones on a schedule. Test the initial one.
+  read ts fac version timeval date time ntp junk
+  WVPASSEQ "$fac" "T:"
+  WVFAIL contains "$version" ' '
+  WVPASS [ "$timeval" -gt 0 ]
+  WVPASSEQ "$ntp" "ntp=1"
+  WVPASSEQ "$junk" ""
+
+  # our actual log messages from above.
+  read ts fac msg
+  WVPASSEQ "$fac" "foo:"
+  WVPASSEQ "$msg" "blue message ☺"
+
+  read ts fac msg
+  WVPASSEQ "$fac" "CRON:"
+  WVPASSEQ "$msg" "pam_unix(cron:session): session closed for user root"
+
+  read ts fac msg
+  WVPASSEQ "$fac" "apenwarr:"
+  WVPASSEQ "$msg" "hello world"
+}
diff --git a/logupload/client/testdata/version b/logupload/client/testdata/version
new file mode 100644
index 0000000..257cc56
--- /dev/null
+++ b/logupload/client/testdata/version
@@ -0,0 +1 @@
+foo
diff --git a/logupload/client/upload.c b/logupload/client/upload.c
index e629060..c72d640 100644
--- a/logupload/client/upload.c
+++ b/logupload/client/upload.c
@@ -9,8 +9,8 @@
 #include "kvextract.h"
 #include "utils.h"
 
-#define DEVICE_KEY_PATH "/etc/ssl/private/device.key"
-#define DEVICE_CERT_PATH "/etc/ssl/certs/device.pem"
+#define DEVICE_KEY_PATH "/tmp/ssl/private/device.key"
+#define DEVICE_CERT_PATH "/tmp/ssl/certs/device.pem"
 
 #define FORM_DATA_SPLITTER_PREFIX "foo-splitter-"
 
diff --git a/logupload/client/upload.h b/logupload/client/upload.h
index dbbc89a..5e9052d 100644
--- a/logupload/client/upload.h
+++ b/logupload/client/upload.h
@@ -1,9 +1,17 @@
 #ifndef _H_LOGUPLOAD_CLIENT_UPLOAD_H_
 #define _H_LOGUPLOAD_CLIENT_UPLOAD_H_
 
+#ifdef __cplusplus
+extern "C" {
+#endif
+
 #include "kvextract.h"
 
 int upload_file(const char* server_url, const char* target_name, char* data,
     ssize_t len, struct kvpair* kvpairs);
 
+#ifdef __cplusplus
+}
+#endif
+
 #endif  // _H_LOGUPLOAD_CLIENT_UPLOAD_H_
diff --git a/logupload/client/utils.c b/logupload/client/utils.c
index f508960..6ce3e6b 100644
--- a/logupload/client/utils.c
+++ b/logupload/client/utils.c
@@ -10,15 +10,23 @@
 #include "utils.h"
 
 ssize_t read_file_as_string(const char* file_path, char* data, int length) {
-  if (!file_path || !data)
+  if (!file_path || !data) {
+    errno = EINVAL;
     return -1;
+  }
   int fd = open(file_path, O_RDONLY);
-  if (fd < 0)
+  if (fd < 0) {
+    perror(file_path);
     return -1;
+  }
   ssize_t num_read = read(fd, data, length - 1);
+  if (num_read < 0) {
+    perror(file_path);
+  }
   close(fd);
-  if (num_read < 0)
+  if (num_read < 0) {
     return -1;
+  }
   data[num_read] = '\0';
   return num_read;
 }
@@ -40,9 +48,14 @@
   char buf[64];
   ssize_t num_read;
   int fd = open(file_path, O_RDONLY);
-  if (fd < 0)
+  if (fd < 0) {
+    perror(file_path);
     return 0;
+  }
   num_read = read(fd, buf, sizeof(buf) -1);
+  if (num_read < 0) {
+    perror(file_path);
+  }
   close(fd);
   if (num_read > 0) {
     buf[num_read] = '\0';
@@ -54,7 +67,7 @@
 int write_file_as_uint64(const char* file_path, uint64_t counter) {
   int fd = open(file_path, O_WRONLY | O_CREAT, RW_FILE_PERMISSIONS);
   if (fd < 0) {
-    fprintf(stderr, "could not open file %s: %s\n", file_path, strerror(errno));
+    perror(file_path);
     return -1;
   }
   char data[64];
@@ -62,8 +75,7 @@
   ssize_t num_written = write(fd, data, len);
   close(fd);
   if (num_written < len) {
-    fprintf(stderr, "failed writing to file %s:  %s\n", file_path,
-        strerror(errno));
+    perror(file_path);
     return -1;
   }
   return 0;
@@ -72,15 +84,12 @@
 ssize_t write_to_file(const char* file_path, const char* data) {
   int fd = open(file_path, O_WRONLY | O_CREAT | O_APPEND, RW_FILE_PERMISSIONS);
   if (fd < 0) {
-    fprintf(stderr, "could not open %s for writing: %s\n", file_path,
-        strerror(errno));
     return -1;
   }
   ssize_t len = strlen(data);
   ssize_t num_written = write(fd, data, len);
   close(fd);
   if (num_written < len) {
-    fprintf(stderr, "failed writing to %s: %s\n", file_path, strerror(errno));
     return -1;
   }
   return num_written;
@@ -122,7 +131,7 @@
 
 int deflate_inplace(z_stream *strm, unsigned char* buf,
     unsigned long len, unsigned long *out_len) {
-  int rv, used_out, used_in;
+  int rv;
   unsigned char temp[1024]; // big enough to hold the header which is 11 bytes,
                             // plus more so we can eat into our data quicker
 
@@ -140,22 +149,15 @@
   strm->avail_out = sizeof(temp);
 
   // Do the first compression step
-  rv = deflate(strm, Z_FINISH);
+  rv = deflate(strm, Z_NO_FLUSH);
   if (rv == Z_STREAM_ERROR)
     return rv;
-  used_out = strm->next_out - temp;
-  used_in = len - strm->avail_in;
-  if (used_out > used_in) {
-    // Our output data is larger than the input data consumed, sad panda :(
-    return Z_BUF_ERROR;
-  }
+  int temp_used = strm->next_out - temp;
 
-  // Copy the data back to our buffer and use that from now on.
-  memcpy(buf, temp, used_out);
-  strm->next_out = buf + used_out;
+  strm->next_out = buf;
   while (rv == Z_OK) {
     // Update how much room we have in the output buffer, there may be a point
-    // where its consumed all the input data and we just need to provide more
+    // where it's consumed all the input data and we just need to provide more
     // output data as well.
     if (strm->avail_in) {
       strm->avail_out = strm->next_in - strm->next_out;
@@ -168,7 +170,13 @@
   }
   if (rv == Z_STREAM_END) {
     // Successfully did the compression.
-    *out_len = strm->next_out - buf;
+    if (strm->avail_out < temp_used) {
+      fprintf(stderr, "zlib problem: out is larger than in!\n");
+      return Z_BUF_ERROR;
+    }
+    memmove(buf + temp_used, buf, strm->next_out - buf);
+    memcpy(buf, temp, temp_used);
+    *out_len = strm->next_out - buf + temp_used;
     return Z_OK;
   }
 
diff --git a/logupload/client/utils.h b/logupload/client/utils.h
index 8609cce..6901634 100644
--- a/logupload/client/utils.h
+++ b/logupload/client/utils.h
@@ -1,6 +1,10 @@
 #ifndef _H_LOGUPLOAD_CLIENT_UTILS_H_
 #define _H_LOGUPLOAD_CLIENT_UTILS_H_
 
+#ifdef __cplusplus
+extern "C" {
+#endif
+
 #include <fcntl.h>
 #include <inttypes.h>
 #include <sys/stat.h>
@@ -50,4 +54,9 @@
 // called.
 int deflate_inplace(z_stream *strm, unsigned char* buf,
     unsigned long len, unsigned long *out_len);
+
+#ifdef __cplusplus
+}
+#endif
+
 #endif  // _H_LOGUPLOAD_CLIENT_UTILS_H_
diff --git a/logupload/client/utils_test.c b/logupload/client/utils_test.cc
similarity index 70%
rename from logupload/client/utils_test.c
rename to logupload/client/utils_test.cc
index a639c7a..fbcea7f 100644
--- a/logupload/client/utils_test.c
+++ b/logupload/client/utils_test.cc
@@ -107,25 +107,55 @@
   EXPECT_EQ(-1, parse_line_data(buf4, &data));
 }
 
-TEST(Utils, deflate_in_place_test) {
-  // We only test the success case because the failure cases shouldn't occur or
-  // require incompressible data which we will never encounter in ASCII logs.
+
+#define RANDBUF 16384
+
+static void zlib_test(int modulus, unsigned long datalen, unsigned long maxlen,
+    int expect_ok) {
   z_stream zstrm;
   memset(&zstrm, 0, sizeof(zstrm));
-  unsigned char random_data[16384];
-  // Use only 7 bits of data so its closer to ASCII chars and will compress.
+  unsigned char random_data[RANDBUF];
   for (unsigned int i = 0; i < sizeof(random_data); i++) {
-    random_data[i] = random() % 128;
+    random_data[i] = random() % modulus;
   }
-  unsigned char backup_data[16384];
+  unsigned char backup_data[RANDBUF];
   memcpy(backup_data, random_data, sizeof(random_data));
-  unsigned long comp_size = sizeof(random_data);
-  EXPECT_EQ(Z_OK, deflate_inplace(&zstrm, random_data, sizeof(random_data),
-      &comp_size));
-  unsigned char decompressed[16384];
-  unsigned long full_size = sizeof(decompressed);
-  EXPECT_EQ(Z_OK, uncompress(decompressed, &full_size, random_data,
-      comp_size));
-  EXPECT_EQ(full_size, sizeof(random_data));
-  EXPECT_EQ(0, memcmp(decompressed, backup_data, sizeof(backup_data)));
+  unsigned long comp_size = maxlen;
+  int rv = deflate_inplace(&zstrm, random_data, datalen, &comp_size);
+  if (expect_ok) {
+    EXPECT_EQ(Z_OK, rv);
+    unsigned char decompressed[RANDBUF];
+    unsigned long full_size = sizeof(decompressed);
+    EXPECT_EQ(Z_OK, uncompress(decompressed, &full_size, random_data,
+        comp_size));
+    EXPECT_EQ(full_size, datalen);
+    EXPECT_EQ(0, memcmp(decompressed, backup_data, datalen));
+  } else {
+    EXPECT_NE(Z_OK, rv);
+  }
+}
+
+TEST(Utils, deflate_in_place1_test) {
+  // 7-bit compressible data
+  zlib_test(128, RANDBUF, RANDBUF, 1);
+}
+
+TEST(Utils, deflate_in_place2_test) {
+  // 8-bit uncompressible data
+  zlib_test(256, RANDBUF, RANDBUF, 0);
+}
+
+TEST(Utils, deflate_in_place3_test) {
+  // 8-bit, but room for *only* 11-byte zlib header.
+  // zlib should choose to "store" the data directly (with header) rather
+  // than ever making it larger, so this should always fit.
+  zlib_test(256, RANDBUF-11, RANDBUF, 1);
+}
+
+TEST(Utils, deflate_in_place4_test) {
+  zlib_test(256, 0, RANDBUF, 1);
+}
+
+TEST(Utils, deflate_in_place5_test) {
+  zlib_test(256, 1, RANDBUF, 1);
 }
diff --git a/logupload/client/wvtest b/logupload/client/wvtest
new file mode 120000
index 0000000..f86b784
--- /dev/null
+++ b/logupload/client/wvtest
@@ -0,0 +1 @@
+../../cmds/wvtest
\ No newline at end of file
diff --git a/signing/repack.py b/signing/repack.py
index 879f61b..4e06fc1 100755
--- a/signing/repack.py
+++ b/signing/repack.py
@@ -4,7 +4,19 @@
 """Repackage image for signing check.
 
 The image is packaged as follows
-Signing will add 8 bytes header (header size and signature offset).
+Signing will add 16 bytes header (header size and signature offset) plus
+8 bytes of padding.
+
+There are two cases:
+  if signature_offset == FAKE_SIGN_OFFSET
+   header_size contains the length of the file after the 16 byte header
+    (or file_size = 16 + header_size)
+
+  if signature_offset != FAKE_SIGN_OFFSET
+    header_size =  signature_offset - 16 (not needed/used)
+    signature_offset = filesize - 256 - 16 (means the signature is 256 bytes)
+
+
 The info is free format. Now it is only a string to hold verity table.
 |-------------------| <=== Byte 0
 | header size       | (integer)
diff --git a/taxonomy/Makefile b/taxonomy/Makefile
index 6a04a8b..6c20ed6 100644
--- a/taxonomy/Makefile
+++ b/taxonomy/Makefile
@@ -4,7 +4,7 @@
 
 test:
 	set -e; \
-	for d in $(wildcard *_test.py); do \
+	for d in $(wildcard tests/*_test.py); do \
 		PYTHONPATH=. $(PYTHON) $$d; \
 	done
 
diff --git a/taxonomy/ethernet.py b/taxonomy/ethernet.py
index 9b994e7..eee8057 100644
--- a/taxonomy/ethernet.py
+++ b/taxonomy/ethernet.py
@@ -63,6 +63,7 @@
     '90:b6:86': 'samsung',
     '90:e7:c4': 'samsung',
     'a0:0b:ba': 'samsung',
+    'b0:df:3a': 'samsung',
     'c0:bd:d1': 'samsung',
     'c4:42:02': 'samsung',
     'cc:3a:61': 'samsung',
diff --git a/taxonomy/dhcp_test.py b/taxonomy/tests/dhcp_test.py
similarity index 100%
rename from taxonomy/dhcp_test.py
rename to taxonomy/tests/dhcp_test.py
diff --git a/taxonomy/ethernet_test.py b/taxonomy/tests/ethernet_test.py
similarity index 100%
rename from taxonomy/ethernet_test.py
rename to taxonomy/tests/ethernet_test.py
diff --git a/taxonomy/ssdp_test.py b/taxonomy/tests/ssdp_test.py
similarity index 100%
rename from taxonomy/ssdp_test.py
rename to taxonomy/tests/ssdp_test.py
diff --git a/taxonomy/wifi_test.py b/taxonomy/tests/wifi_test.py
similarity index 100%
rename from taxonomy/wifi_test.py
rename to taxonomy/tests/wifi_test.py
diff --git a/taxonomy/wifi.py b/taxonomy/wifi.py
index b2c0651..2509685 100644
--- a/taxonomy/wifi.py
+++ b/taxonomy/wifi.py
@@ -348,11 +348,14 @@
     'wifi|probe:0,1,50,45,221(0050f2,8),htcap:012c,htagg:03,htmcs:000000ff|assoc:0,1,50,48,45,221(0050f2,2),127,htcap:012c,htagg:03,htmcs:000000ff,extcap:020a0000|oui:asus':
         ('QCA_WCN3660', 'Nexus 7 (2013)', '2.4GHz'),
 
-    'wifi|probe:0,1,50,3,45,127,221(0050f2,4),221(506f9a,9),221(001018,2),htcap:002d,wps:Nexus_9|assoc:0,1,50,33,36,48,45,127,221(001018,2),221(0050f2,2),htcap:002d':
+    'wifi|probe:0,1,50,3,45,127,221(0050f2,4),221(506f9a,9),221(001018,2),htcap:002d,wps:Nexus_9|assoc:0,1,50,33,36,48,45,127,221(001018,2),221(0050f2,2),htcap:002d|oui:htc':
         ('BCM4354', 'Nexus 9', '5GHz'),
-    'wifi|probe:0,1,50,3,45,127,221(0050f2,4),221(506f9a,9),221(001018,2),htcap:002d,wps:Nexus_9|assoc:0,1,50,33,36,48,45,127,221(001018,2),221(0050f2,2),htcap:002d':
+    'wifi|probe:0,1,50,3,45,127,221(0050f2,4),221(506f9a,9),221(001018,2),htcap:002d,wps:Nexus_9|assoc:0,1,50,33,36,48,45,127,221(001018,2),221(0050f2,2),htcap:002d|oui:htc':
         ('BCM4354', 'Nexus 9', '2.4GHz'),
 
+    'wifi|probe:0,1,45,221(001018,2),221(00904c,51),htcap:01fe,htagg:1b,htmcs:0000ffff|assoc:0,1,48,45,221(001018,2),221(00904c,51),221(0050f2,2),htcap:01fe,htagg:1b,htmcs:0000ffff|oui:samsung':
+        ('', 'Nexus 10', '5GHz'),
+
     'wifi|probe:0,1,45,127,191,221(0050f2,4),221(506f9a,9),221(001018,2),htcap:006f,vhtcap:0f815832,wps:Nexus_Player|assoc:0,1,33,36,48,45,127,191,221(001018,2),221(0050f2,2),htcap:006f,vhtcap:0f815832':
         ('BCM4356', 'Nexus Player', '5GHz'),
     'wifi|probe:0,1,50,3,45,127,221(0050f2,4),221(506f9a,9),221(001018,2),htcap:002d,wps:Nexus_Player|assoc:0,1,50,33,36,48,45,127,221(001018,2),221(0050f2,2),htcap:002d':
diff --git a/waveguide/Makefile b/waveguide/Makefile
index 62c875e..c75da49 100644
--- a/waveguide/Makefile
+++ b/waveguide/Makefile
@@ -39,7 +39,7 @@
 
 install:
 	mkdir -p $(LIBDIR) $(BINDIR)
-	$(INSTALL) -m 0644 *.py $(LIBDIR)/
+	$(INSTALL) -m 0644 $(filter-out %_test.py, $(wildcard *.py)) $(LIBDIR)/
 	$(INSTALL) -m 0755 waveguide.py $(LIBDIR)/
 	$(INSTALL) -m 0755 waveguide $(BINDIR)/
 
diff --git a/wifi/Makefile b/wifi/Makefile
index 120d541..c6aa330 100644
--- a/wifi/Makefile
+++ b/wifi/Makefile
@@ -13,7 +13,7 @@
 all:
 
 %.test: %_test.py
-	./$<
+	PYTHONPATH=..:$(PYTHONPATH) ./$<
 
 runtests: \
 	$(patsubst %_test.py,%.test,$(wildcard *_test.py))
@@ -22,7 +22,7 @@
 	$(GPYLINT) $^
 
 test_only: all
-	$(MAKE) runtests
+	./wvtest/wvtestrun $(MAKE) runtests
 
 # Use a submake here, only because otherwise GNU make (3.81) will not print
 # an error about 'test' itself failing if one of the two sub-targets fails.
@@ -34,7 +34,7 @@
 
 install:
 	mkdir -p $(LIBDIR) $(BINDIR)
-	$(INSTALL) -m 0644 *.py $(LIBDIR)/
+	$(INSTALL) -m 0644 $(filter-out %_test.py, $(wildcard *.py)) $(LIBDIR)/
 	$(INSTALL) -m 0755 wifi.py $(LIBDIR)/
 	rm -f $(BINDIR)/wifi
 	ln -s /usr/wifi/wifi.py $(BINDIR)/wifi
diff --git a/wifi/iw.py b/wifi/iw.py
index 6f70806..a8ffdb4 100644
--- a/wifi/iw.py
+++ b/wifi/iw.py
@@ -65,7 +65,8 @@
     **kwargs: Passed to the underlying subprocess call.
 
   Returns:
-    A dict of the the form: {'phyX': 'frequency_and_channel': [(F, C), ...]}
+    A dict of the the form: {'phyX': {'frequency_and_channel': [(F, C), ...],
+                                      'bands': set(band1, ...)}, ...}
   """
   result = {}
   phy = None
@@ -74,12 +75,18 @@
     match = _WIPHY_RE.match(line)
     if match:
       phy = match.group('phy')
-      result[phy] = {'frequency_and_channel': []}
+      result[phy] = {'frequency_and_channel': [], 'bands': set()}
     else:
       match = _FREQUENCY_AND_CHANNEL_RE.match(line)
       if match:
-        result[phy]['frequency_and_channel'].append(
-            (match.group('frequency'), match.group('channel')))
+        frequency, channel = match.group('frequency'), match.group('channel')
+        result[phy]['frequency_and_channel'].append((frequency, channel))
+        if frequency.startswith('2'):
+          result[phy]['bands'].add('2.4')
+        elif frequency.startswith('5'):
+          result[phy]['bands'].add('5')
+        else:
+          utils.log('Unrecognized frequency %s', frequency)
 
   return result
 
@@ -310,3 +317,32 @@
   """
   return utils.subprocess_output_or_none(
       ['iw', 'dev', interface, 'station', 'dump'])
+
+
+def phy_bands(which_phy):
+  """Returns the bands supported by the given phy.
+
+  If a phy P supports more than one band, and another phy Q supports only one of
+  those bands, P is not said to support that band.
+
+  Args:
+    which_phy: The phy for which to get bands.
+
+  Returns:
+    The bands supported by the given phy.
+  """
+
+  band_phys = {}
+  for phy, info in phy_parsed().iteritems():
+    bands = info['bands']
+    for band in bands:
+      band_phys.setdefault(band, phy)
+    if len(bands) == 1:
+      band_phys[list(bands)[0]] = phy
+
+  result = set()
+  for band, phy in band_phys.iteritems():
+    if which_phy == phy:
+      result.add(band)
+
+  return result
diff --git a/wifi/iw_test.py b/wifi/iw_test.py
index f2ddc02..4a7ef4c 100755
--- a/wifi/iw_test.py
+++ b/wifi/iw_test.py
@@ -553,5 +553,20 @@
   INTERFACE_INFO_OUTPUT = hold
 
 
+@wvtest.wvtest
+def phy_bands_test():
+  # phy0 claims to support 5 GHz, but phy1 only supports 5 GHz and so it
+  # supercedes it.
+  wvtest.WVPASSEQ(set(['2.4']), iw.phy_bands('phy0'))
+  wvtest.WVPASSEQ(set(['5']), iw.phy_bands('phy1'))
+
+  # Now remove phy1 from the 'iw phy' output and see that phy0 gets both bands.
+  global PHY_OUTPUT
+  hold = PHY_OUTPUT
+  PHY_OUTPUT = PHY_OUTPUT[PHY_OUTPUT.find('Wiphy phy0'):]
+  wvtest.WVPASSEQ(set(['2.4', '5']), iw.phy_bands('phy0'))
+  PHY_OUTPUT = hold
+
+
 if __name__ == '__main__':
   wvtest.wvtest_main()
diff --git a/wifi/wifi.py b/wifi/wifi.py
index 37621d3..73b717d 100755
--- a/wifi/wifi.py
+++ b/wifi/wifi.py
@@ -923,6 +923,10 @@
   if success:
     if extra[0] == 'set':
       if opt.persist:
+        phy = iw.find_phy(opt.band, opt.channel)
+        for band in iw.phy_bands(phy):
+          if band != opt.band:
+            persist.delete_options('hostapd', band)
         persist.save_options('hostapd', opt.band, argv)
       persist.save_options('hostapd', opt.band, argv, tmp=True)