diff --git a/README.md b/README.md
index fdf69259f9e01e0a4dc52ee9e635f3e3819b51da..b7dc446f32b5270075bc96213d17d7742a207cf7 100644
--- a/README.md
+++ b/README.md
@@ -49,3 +49,13 @@ a single attribute *key*.
 `/api/close` (*CloseRequest*)
 
 Forget the key for a given user.
+
+# Dovecot integration
+
+The final consumer for user encryption keys is the Dovecot
+service. The *dovecot-keylookupd* daemon can read the user public and
+private keys from LDAP, and serve the *unencrypted* keys to Dovecot
+using its [dict proxy
+protocol](https://wiki2.dovecot.org/AuthDatabase/Dict).
+
+TODO: explain the lookup protocol.
diff --git a/backend/ldap.go b/backend/ldap.go
index 03c2b755ea19ac057ea565231a2b3ac9401d7f91..aa10edd5c23b083f9d1c0e3bff44734f67451cdd 100644
--- a/backend/ldap.go
+++ b/backend/ldap.go
@@ -19,10 +19,12 @@ type LDAPQueryConfig struct {
 	// a query.
 	SearchBase   string `yaml:"search_base"`
 	SearchFilter string `yaml:"search_filter"`
-	Scope        string `yaml:"scope"`
+	ScopeStr     string `yaml:"scope"`
+	scope        int
 
 	// Attr is the LDAP attribute holding the encrypted user key.
-	Attr string `yaml:"attr"`
+	PublicKeyAttr  string `yaml:"attr"`
+	PrivateKeyAttr string `yaml:"attr"`
 }
 
 // Valid returns an error if the configuration is invalid.
@@ -33,32 +35,36 @@ func (c *LDAPQueryConfig) Valid() error {
 	if c.SearchFilter == "" {
 		return errors.New("empty search_filter")
 	}
-	if c.Scope != "one" && c.Scope != "sub" {
-		return errors.New("unknown scope")
+	c.scope = ldap.ScopeWholeSubtree
+	if c.ScopeStr != "" {
+		s, err := ldaputil.ParseScope(c.ScopeStr)
+		if err != nil {
+			return err
+		}
+		c.scope = s
 	}
-	if c.Attr == "" {
-		return errors.New("empty attr")
+	if c.PublicKeyAttr == "" {
+		return errors.New("empty public_key_attr")
+	}
+	if c.PrivateKeyAttr == "" {
+		return errors.New("empty public_key_attr")
 	}
 	return nil
 }
 
-func (c *LDAPQueryConfig) searchRequest(username string) *ldap.SearchRequest {
+func (c *LDAPQueryConfig) searchRequest(username string, attrs ...string) *ldap.SearchRequest {
 	u := ldap.EscapeFilter(username)
 	base := strings.Replace(c.SearchBase, "%s", u, -1)
 	filter := strings.Replace(c.SearchFilter, "%s", u, -1)
-	scope := ldap.ScopeWholeSubtree
-	if c.Scope == "one" {
-		scope = ldap.ScopeSingleLevel
-	}
 	return ldap.NewSearchRequest(
 		base,
-		scope,
+		c.scope,
 		ldap.NeverDerefAliases,
 		0,
 		0,
 		false,
 		filter,
-		[]string{c.Attr},
+		attrs,
 		nil,
 	)
 }
@@ -117,8 +123,8 @@ func NewLDAPBackend(config *LDAPConfig) (*ldapBackend, error) {
 	}, nil
 }
 
-func (b *ldapBackend) GetKeys(ctx context.Context, username string) [][]byte {
-	result, err := b.pool.Search(ctx, b.config.Query.searchRequest(username))
+func (b *ldapBackend) GetPrivateKeys(ctx context.Context, username string) [][]byte {
+	result, err := b.pool.Search(ctx, b.config.Query.searchRequest(username, b.config.Query.PrivateKeyAttr))
 	if err != nil {
 		log.Printf("LDAP error: %v", err)
 		return nil
@@ -126,8 +132,30 @@ func (b *ldapBackend) GetKeys(ctx context.Context, username string) [][]byte {
 
 	var out [][]byte
 	for _, ent := range result.Entries {
-		k := []byte(ent.GetAttributeValue(b.config.Query.Attr))
-		out = append(out, k)
+		for _, val := range ent.GetAttributeValues(b.config.Query.PrivateKeyAttr) {
+			out = append(out, []byte(val))
+		}
 	}
 	return out
 }
+
+func (b *ldapBackend) GetPublicKey(ctx context.Context, username string) []byte {
+	result, err := b.pool.Search(ctx, b.config.Query.searchRequest(username, b.config.Query.PublicKeyAttr))
+	if err != nil {
+		log.Printf("LDAP error: %v", err)
+		return nil
+	}
+	if len(result.Entries) == 0 {
+		return nil
+	}
+	if len(result.Entries) > 1 {
+		log.Printf("public key query for %s returned too many results (%d)", username, len(result.Entries))
+		return nil
+	}
+
+	s := result.Entries[0].GetAttributeValue(b.config.Query.PublicKeyAttr)
+	if s == "" {
+		return nil
+	}
+	return []byte(s)
+}
diff --git a/cmd/dovecot-keylookupd/main.go b/cmd/dovecot-keylookupd/main.go
new file mode 100644
index 0000000000000000000000000000000000000000..8416d52ee12a718b42e65e1dff7025ee7356abd7
--- /dev/null
+++ b/cmd/dovecot-keylookupd/main.go
@@ -0,0 +1,85 @@
+package main
+
+import (
+	"flag"
+	"io/ioutil"
+	"log"
+	"os"
+	"os/signal"
+	"syscall"
+	"time"
+
+	"git.autistici.org/ai3/go-common/unix"
+	"github.com/coreos/go-systemd/daemon"
+	"gopkg.in/yaml.v2"
+
+	"git.autistici.org/id/keystore/dovecot"
+)
+
+var (
+	configFile              = flag.String("config", "/etc/keystore/dovecot.yml", "path of config file")
+	socketPath              = flag.String("socket", "/run/dovecot-keystored/socket", "`path` to the UNIX socket to listen on")
+	systemdSocketActivation = flag.Bool("systemd-socket", false, "use SystemD socket activation")
+	requestTimeout          = flag.Duration("timeout", 5*time.Second, "timeout for incoming requests")
+)
+
+// Read YAML config.
+func loadConfig() (*dovecot.Config, error) {
+	data, err := ioutil.ReadFile(*configFile)
+	if err != nil {
+		return nil, err
+	}
+	var config dovecot.Config
+	if err := yaml.Unmarshal(data, &config); err != nil {
+		return nil, err
+	}
+	return &config, nil
+}
+
+func main() {
+	log.SetFlags(0)
+	flag.Parse()
+
+	syscall.Umask(007)
+
+	config, err := loadConfig()
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	ddp, err := dovecot.NewKeyLookupProxy(config)
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	srv := unix.NewLineServer(dovecot.NewDictProxyServer(ddp))
+	srv.RequestTimeout = *requestTimeout
+
+	var sockSrv *unix.SocketServer
+	if *systemdSocketActivation {
+		sockSrv, err = unix.NewSystemdSocketServer(srv)
+	} else {
+		sockSrv, err = unix.NewUNIXSocketServer(*socketPath, srv)
+	}
+	if err != nil {
+		log.Fatalf("error: %v", err)
+	}
+
+	done := make(chan struct{})
+	sigCh := make(chan os.Signal, 1)
+	go func() {
+		<-sigCh
+		log.Printf("terminating")
+		sockSrv.Close()
+		close(done)
+	}()
+	signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
+
+	log.Printf("starting")
+	daemon.SdNotify(false, "READY=1")
+	if err := sockSrv.Serve(); err != nil {
+		log.Fatal(err)
+	}
+
+	<-done
+}
diff --git a/dovecot/dict.go b/dovecot/dict.go
new file mode 100644
index 0000000000000000000000000000000000000000..8278e3c4a657069153d25984ae478552fdd7953c
--- /dev/null
+++ b/dovecot/dict.go
@@ -0,0 +1,71 @@
+package dovecot
+
+import (
+	"bytes"
+	"context"
+	"encoding/json"
+	"errors"
+
+	"git.autistici.org/ai3/go-common/unix"
+)
+
+var (
+	failResponse    = []byte{'F', '\n'}
+	noMatchResponse = []byte{'N', '\n'}
+)
+
+// DictDatabase is an interface to a key/value store by way of the Lookup
+// method.
+type DictDatabase interface {
+	Lookup(context.Context, string) (interface{}, bool)
+}
+
+// DictProxyServer exposes a Database using the Dovecot dict proxy
+// protocol (see https://wiki2.dovecot.org/AuthDatabase/Dict).
+//
+// It implements the unix.LineHandler interface from the
+// ai3/go-common/unix package.
+type DictProxyServer struct {
+	db DictDatabase
+}
+
+// NewDictProxyServer creates a new DictProxyServer.
+func NewDictProxyServer(db DictDatabase) *DictProxyServer {
+	return &DictProxyServer{db: db}
+}
+
+// ServeLine handles a single command.
+func (p *DictProxyServer) ServeLine(ctx context.Context, lw unix.LineResponseWriter, line []byte) error {
+	if len(line) < 1 {
+		return errors.New("line too short")
+	}
+
+	switch line[0] {
+	case 'H':
+		return p.handleHello(ctx, lw, line[1:])
+	case 'L':
+		return p.handleLookup(ctx, lw, line[1:])
+	default:
+		return lw.WriteLine(failResponse)
+	}
+}
+
+func (p *DictProxyServer) handleHello(ctx context.Context, lw unix.LineResponseWriter, arg []byte) error {
+	// TODO: parse the hello line and extract useful information.
+	return nil
+}
+
+func (p *DictProxyServer) handleLookup(ctx context.Context, lw unix.LineResponseWriter, arg []byte) error {
+	obj, ok := p.db.Lookup(ctx, string(arg))
+	if !ok {
+		return lw.WriteLine(noMatchResponse)
+	}
+
+	var buf bytes.Buffer
+	buf.Write([]byte{'O'})
+	if err := json.NewEncoder(&buf).Encode(obj); err != nil {
+		return err
+	}
+	buf.Write([]byte{'\n'})
+	return lw.WriteLine(buf.Bytes())
+}
diff --git a/dovecot/keyproxy.go b/dovecot/keyproxy.go
new file mode 100644
index 0000000000000000000000000000000000000000..9be2a1039f1789547d42f02a49aa37995df807f9
--- /dev/null
+++ b/dovecot/keyproxy.go
@@ -0,0 +1,124 @@
+package dovecot
+
+import (
+	"context"
+	"encoding/base64"
+	"errors"
+	"strings"
+
+	"git.autistici.org/ai3/go-common/clientutil"
+
+	"git.autistici.org/id/keystore/backend"
+	"git.autistici.org/id/keystore/client"
+	"git.autistici.org/id/keystore/userenckey"
+)
+
+// Config for the dovecot-keystore daemon.
+type Config struct {
+	Shard      string                    `yaml:"shard"`
+	LDAPConfig *backend.LDAPConfig       `yaml:"ldap"`
+	Keystore   *clientutil.BackendConfig `yaml:"keystore"`
+}
+
+// Database represents the interface to the underlying backend for
+// encrypted user keys.
+type Database interface {
+	GetPublicKey(context.Context, string) []byte
+	GetPrivateKeys(context.Context, string) [][]byte
+}
+
+func (c *Config) check() error {
+	if c.LDAPConfig == nil {
+		return errors.New("missing backend config")
+	}
+	return c.LDAPConfig.Valid()
+}
+
+type userdbResponse struct {
+	PublicKey string `json:"mail_crypt_global_public_key"`
+}
+
+type passdbResponse struct {
+	PrivateKey string `json:"mail_crypt_global_private_key"`
+}
+
+var passwordSep = "/"
+
+// KeyLookupProxy interfaces Dovecot with the user encryption key database.
+type KeyLookupProxy struct {
+	config   *Config
+	keystore client.Client
+	db       Database
+}
+
+func NewKeyLookupProxy(config *Config) (*KeyLookupProxy, error) {
+	if err := config.check(); err != nil {
+		return nil, err
+	}
+
+	ksc, err := client.New(config.Keystore)
+	if err != nil {
+		return nil, err
+	}
+
+	// There is only one supported backend type, ldap.
+	ldap, err := backend.NewLDAPBackend(config.LDAPConfig)
+	if err != nil {
+		return nil, err
+	}
+
+	return &KeyLookupProxy{
+		config:   config,
+		keystore: ksc,
+		db:       ldap,
+	}, nil
+}
+
+// Lookup a key using the dovecot dict proxy interface.
+//
+// We can be sent a userdb lookup, or a passdb lookup, and we can tell
+// them apart from the structure of the key:
+//
+// If it contains passwordSep, then it's a passdb lookup and the key
+// consists of 'username' and 'password'. Otherwise, it's a userdb
+// lookup and the key is simply the username.
+func (s *KeyLookupProxy) Lookup(ctx context.Context, key string) (interface{}, bool) {
+	if strings.Contains(key, passwordSep) {
+		kparts := strings.SplitN(key, passwordSep, 2)
+		return s.lookupPassdb(ctx, kparts[0], kparts[1])
+	}
+	return s.lookupUserdb(ctx, key)
+}
+
+func (s *KeyLookupProxy) lookupUserdb(ctx context.Context, username string) (interface{}, bool) {
+	pub := s.db.GetPublicKey(ctx, username)
+	if pub == nil {
+		return nil, false
+	}
+	return &userdbResponse{PublicKey: b64encode(pub)}, true
+}
+
+func (s *KeyLookupProxy) lookupPassdb(ctx context.Context, username, password string) (interface{}, bool) {
+	// If the password is a SSO token, try to fetch the
+	// unencrypted key from the keystore daemon.
+	priv, err := s.keystore.Get(ctx, s.config.Shard, username, password)
+	if err == nil {
+		return &passdbResponse{PrivateKey: b64encode(priv)}, true
+	}
+
+	// Otherwise, fetch encrypted keys from the db and attempt to
+	// decrypt them.
+	encKeys := s.db.GetPrivateKeys(ctx, username)
+	if len(encKeys) == 0 {
+		return nil, false
+	}
+	priv, err = userenckey.Decrypt(encKeys, []byte(password))
+	if err != nil {
+		return nil, false
+	}
+	return &passdbResponse{PrivateKey: b64encode(priv)}, true
+}
+
+func b64encode(b []byte) string {
+	return base64.StdEncoding.EncodeToString(b)
+}
diff --git a/server/keystore.go b/server/keystore.go
index 8daf03124c7bdf43e14b0e9a62a80a635b9bdbc8..9cc820b36f1142e57776e95626afa6668cdb2354 100644
--- a/server/keystore.go
+++ b/server/keystore.go
@@ -11,19 +11,19 @@ import (
 	"git.autistici.org/id/go-sso"
 
 	"git.autistici.org/id/keystore/backend"
+	"git.autistici.org/id/keystore/userenckey"
 )
 
 var (
-	ErrNoKeys      = errors.New("no keys available")
-	ErrBadPassword = errors.New("could not decrypt key with password")
-	ErrBadUser     = errors.New("username does not match authentication token")
-	ErrInvalidTTL  = errors.New("invalid ttl")
+	ErrNoKeys     = errors.New("no keys available")
+	ErrBadUser    = errors.New("username does not match authentication token")
+	ErrInvalidTTL = errors.New("invalid ttl")
 )
 
 // Database represents the interface to the underlying backend for
 // encrypted user keys.
 type Database interface {
-	GetKeys(context.Context, string) [][]byte
+	GetPrivateKeys(context.Context, string) [][]byte
 }
 
 type userKey struct {
@@ -54,7 +54,7 @@ func (c *Config) check() error {
 	if c.LDAPConfig == nil {
 		return errors.New("missing backend config")
 	}
-	return nil
+	return c.LDAPConfig.Valid()
 }
 
 // KeyStore holds decrypted secrets for users in memory for a short
@@ -133,21 +133,16 @@ func (s *KeyStore) Open(ctx context.Context, username, password string, ttlSecon
 		return ErrInvalidTTL
 	}
 
-	encKeys := s.db.GetKeys(ctx, username)
+	encKeys := s.db.GetPrivateKeys(ctx, username)
 	if len(encKeys) == 0 {
 		return ErrNoKeys
 	}
 
-	var pkey []byte
-	var err error
-	for _, key := range encKeys {
-		pkey, err = decrypt(key, []byte(password))
-		if err == nil {
-			break
-		}
-	}
+	// Naive and inefficient way of decrypting multiple keys: it
+	// will recompute the kdf every time, which is expensive.
+	pkey, err := userenckey.Decrypt(encKeys, []byte(password))
 	if err != nil {
-		return ErrBadPassword
+		return err
 	}
 
 	s.mx.Lock()
diff --git a/server/decrypt.go b/userenckey/decrypt.go
similarity index 57%
rename from server/decrypt.go
rename to userenckey/decrypt.go
index 0aac0b1aed2ee074c1dc67f3d5bde69936bdd37e..3f59eca698e0ac41e97478f3530272f6c80e9068 100644
--- a/server/decrypt.go
+++ b/userenckey/decrypt.go
@@ -1,4 +1,4 @@
-package server
+package userenckey
 
 import (
 	"errors"
@@ -7,6 +7,8 @@ import (
 	"golang.org/x/crypto/scrypt"
 )
 
+var ErrBadPassword = errors.New("could not decrypt key with password")
+
 const (
 	scryptN = 32768
 	scryptR = 8
@@ -15,7 +17,20 @@ const (
 	saltLen = 32
 )
 
-func decrypt(data, pw []byte) ([]byte, error) {
+// Decrypt one out of multiple keys with the specified password. The
+// keys share the same cleartext, but have been encrypted with
+// different passwords.
+func Decrypt(encKeys [][]byte, pw []byte) ([]byte, error) {
+	for _, key := range encKeys {
+		dec, err := decryptData(key, pw)
+		if err != nil {
+			return dec, err
+		}
+	}
+	return nil, ErrBadPassword
+}
+
+func decryptData(data, pw []byte) ([]byte, error) {
 	// The KDF salt is prepended to the encrypted key.
 	if len(data) < saltLen {
 		return nil, errors.New("short data")
diff --git a/vendor/git.autistici.org/ai3/go-common/ldap/parse.go b/vendor/git.autistici.org/ai3/go-common/ldap/parse.go
new file mode 100644
index 0000000000000000000000000000000000000000..22a285c6c6d4f82694106852293f93f97a0b1f91
--- /dev/null
+++ b/vendor/git.autistici.org/ai3/go-common/ldap/parse.go
@@ -0,0 +1,20 @@
+package ldaputil
+
+import (
+	"fmt"
+
+	"gopkg.in/ldap.v2"
+)
+
+func ParseScope(s string) (int, error) {
+	switch s {
+	case "base":
+		return ldap.ScopeBaseObject, nil
+	case "one":
+		return ldap.ScopeSingleLevel, nil
+	case "sub":
+		return ldap.ScopeWholeSubtree, nil
+	default:
+		return 0, fmt.Errorf("unknown LDAP scope '%s'", s)
+	}
+}
diff --git a/vendor/git.autistici.org/ai3/go-common/ldap/pool.go b/vendor/git.autistici.org/ai3/go-common/ldap/pool.go
index 6d8093e93dccd5d333133633ab8958092a171355..c77d06177cb1b13ed8c7b71bc3facf07a015adc4 100644
--- a/vendor/git.autistici.org/ai3/go-common/ldap/pool.go
+++ b/vendor/git.autistici.org/ai3/go-common/ldap/pool.go
@@ -40,10 +40,12 @@ func (p *ConnectionPool) connect(ctx context.Context) (*ldap.Conn, error) {
 	conn := ldap.NewConn(c, false)
 	conn.Start()
 
-	conn.SetTimeout(time.Until(deadline))
-	if _, err = conn.SimpleBind(ldap.NewSimpleBindRequest(p.bindDN, p.bindPw, nil)); err != nil {
-		conn.Close()
-		return nil, err
+	if p.bindDN != "" {
+		conn.SetTimeout(time.Until(deadline))
+		if _, err = conn.SimpleBind(ldap.NewSimpleBindRequest(p.bindDN, p.bindPw, nil)); err != nil {
+			conn.Close()
+			return nil, err
+		}
 	}
 
 	return conn, err
diff --git a/vendor/git.autistici.org/ai3/go-common/ldap/search.go b/vendor/git.autistici.org/ai3/go-common/ldap/search.go
index 872f6fec3fdb2a8516f7e3da55dddeb972b358f5..db29ba092ea0cb1e1be3db3a753c75c7e1188f3f 100644
--- a/vendor/git.autistici.org/ai3/go-common/ldap/search.go
+++ b/vendor/git.autistici.org/ai3/go-common/ldap/search.go
@@ -10,16 +10,24 @@ import (
 	"git.autistici.org/ai3/go-common/clientutil"
 )
 
-// Treat all errors as potential network-level issues, except for a
-// whitelist of LDAP protocol level errors that we know are benign.
+// Interface matched by net.Error.
+type hasTemporary interface {
+	Temporary() bool
+}
+
+// Treat network errors as temporary. Other errors are permanent by
+// default.
 func isTemporaryLDAPError(err error) bool {
-	ldapErr, ok := err.(*ldap.Error)
-	if !ok {
-		return true
-	}
-	switch ldapErr.ResultCode {
-	case ldap.ErrorNetwork:
-		return true
+	switch v := err.(type) {
+	case *ldap.Error:
+		switch v.ResultCode {
+		case ldap.ErrorNetwork:
+			return true
+		default:
+			return false
+		}
+	case hasTemporary:
+		return v.Temporary()
 	default:
 		return false
 	}
@@ -32,6 +40,7 @@ func (p *ConnectionPool) Search(ctx context.Context, searchRequest *ldap.SearchR
 	err := clientutil.Retry(func() error {
 		conn, err := p.Get(ctx)
 		if err != nil {
+			// Here conn is nil, so we don't need to Release it.
 			if isTemporaryLDAPError(err) {
 				return clientutil.TempError(err)
 			}
@@ -44,7 +53,7 @@ func (p *ConnectionPool) Search(ctx context.Context, searchRequest *ldap.SearchR
 
 		result, err = conn.Search(searchRequest)
 		if err != nil && isTemporaryLDAPError(err) {
-			p.Release(conn, nil)
+			p.Release(conn, err)
 			return clientutil.TempError(err)
 		}
 		p.Release(conn, err)
diff --git a/vendor/git.autistici.org/ai3/go-common/unix/server.go b/vendor/git.autistici.org/ai3/go-common/unix/server.go
new file mode 100644
index 0000000000000000000000000000000000000000..19fd813900e22eaa541a181d529c5b88bc03b05a
--- /dev/null
+++ b/vendor/git.autistici.org/ai3/go-common/unix/server.go
@@ -0,0 +1,231 @@
+package unix
+
+import (
+	"bufio"
+	"context"
+	"errors"
+	"io"
+	"log"
+	"net"
+	"net/textproto"
+	"os"
+	"sync"
+	"sync/atomic"
+	"time"
+
+	"github.com/coreos/go-systemd/activation"
+	"github.com/theckman/go-flock"
+)
+
+// Handler for UNIX socket server connections.
+type Handler interface {
+	ServeConnection(c net.Conn)
+}
+
+// SocketServer accepts connections on a UNIX socket, speaking the
+// line-based wire protocol, and dispatches incoming requests to the
+// wrapped Server.
+type SocketServer struct {
+	l       net.Listener
+	lock    *flock.Flock
+	closing atomic.Value
+	wg      sync.WaitGroup
+	handler Handler
+}
+
+func newServer(l net.Listener, lock *flock.Flock, h Handler) *SocketServer {
+	s := &SocketServer{
+		l:       l,
+		lock:    lock,
+		handler: h,
+	}
+	s.closing.Store(false)
+	return s
+}
+
+// NewUNIXSocketServer returns a new SocketServer listening on the given path.
+func NewUNIXSocketServer(socketPath string, h Handler) (*SocketServer, error) {
+	// The simplest workflow is: create a new socket, remove it on
+	// exit. However, if the program crashes, the socket might
+	// stick around and prevent the next execution from starting
+	// successfully. We could remove it before starting, but that
+	// would be dangerous if another instance was listening on
+	// that socket. So we wrap socket access with a file lock.
+	lock := flock.NewFlock(socketPath + ".lock")
+	locked, err := lock.TryLock()
+	if err != nil {
+		return nil, err
+	}
+	if !locked {
+		return nil, errors.New("socket is locked by another process")
+	}
+
+	addr, err := net.ResolveUnixAddr("unix", socketPath)
+	if err != nil {
+		return nil, err
+	}
+
+	// Always try to unlink the socket before creating it.
+	os.Remove(socketPath)
+
+	l, err := net.ListenUnix("unix", addr)
+	if err != nil {
+		return nil, err
+	}
+
+	return newServer(l, lock, h), nil
+}
+
+// NewSystemdSocketServer uses systemd socket activation, receiving
+// the open socket as a file descriptor on exec.
+func NewSystemdSocketServer(h Handler) (*SocketServer, error) {
+	listeners, err := activation.Listeners(false)
+	if err != nil {
+		return nil, err
+	}
+	// Our server loop implies a single listener, so find
+	// the first one passed by systemd and ignore all others.
+	// TODO: listen on all fds.
+	for _, l := range listeners {
+		if l != nil {
+			return newServer(l, nil, h), nil
+		}
+	}
+	return nil, errors.New("no available sockets found")
+}
+
+// Close the socket listener and release all associated resources.
+// Waits for active connections to terminate before returning.
+func (s *SocketServer) Close() {
+	s.closing.Store(true)
+	s.l.Close()
+	s.wg.Wait()
+	if s.lock != nil {
+		s.lock.Unlock()
+	}
+}
+
+func (s *SocketServer) isClosing() bool {
+	return s.closing.Load().(bool)
+}
+
+// Serve connections.
+func (s *SocketServer) Serve() error {
+	for {
+		conn, err := s.l.Accept()
+		if err != nil {
+			if s.isClosing() {
+				return nil
+			}
+			return err
+		}
+		s.wg.Add(1)
+		go func() {
+			s.handler.ServeConnection(conn)
+			conn.Close()
+			s.wg.Done()
+		}()
+	}
+}
+
+// LineHandler is the handler for LineServer.
+type LineHandler interface {
+	ServeLine(context.Context, LineResponseWriter, []byte) error
+}
+
+// ErrCloseConnection must be returned by a LineHandler when we want
+// to cleanly terminate the connection without raising an error.
+var ErrCloseConnection = errors.New("close")
+
+// LineResponseWriter writes a single-line response to the underlying
+// connection.
+type LineResponseWriter interface {
+	// WriteLine writes a response (which must include the
+	// line terminator).
+	WriteLine([]byte) error
+
+	// WriteLineCRLF writes a response and adds a line terminator.
+	WriteLineCRLF([]byte) error
+}
+
+// LineServer implements a line-based text protocol. It satisfies the
+// Handler interface.
+type LineServer struct {
+	handler LineHandler
+
+	IdleTimeout    time.Duration
+	WriteTimeout   time.Duration
+	RequestTimeout time.Duration
+}
+
+var (
+	defaultIdleTimeout    = 600 * time.Second
+	defaultWriteTimeout   = 10 * time.Second
+	defaultRequestTimeout = 30 * time.Second
+)
+
+// NewLineServer returns a new LineServer with the given handler and
+// default I/O timeouts.
+func NewLineServer(h LineHandler) *LineServer {
+	return &LineServer{
+		handler:        h,
+		IdleTimeout:    defaultIdleTimeout,
+		WriteTimeout:   defaultWriteTimeout,
+		RequestTimeout: defaultRequestTimeout,
+	}
+}
+
+var crlf = []byte{'\r', '\n'}
+
+type lrWriter struct {
+	*bufio.Writer
+}
+
+func (w *lrWriter) WriteLine(data []byte) error {
+	if _, err := w.Writer.Write(data); err != nil {
+		return err
+	}
+	return w.Writer.Flush()
+}
+
+func (w *lrWriter) WriteLineCRLF(data []byte) error {
+	if _, err := w.Writer.Write(data); err != nil {
+		return err
+	}
+	if _, err := w.Writer.Write(crlf); err != nil {
+		return err
+	}
+	return w.Writer.Flush()
+}
+
+func (l *LineServer) ServeConnection(nc net.Conn) {
+	c := textproto.NewConn(nc)
+	rw := &lrWriter{bufio.NewWriter(nc)}
+	for {
+		nc.SetReadDeadline(time.Now().Add(l.IdleTimeout))
+		line, err := c.ReadLineBytes()
+		if err == io.EOF {
+			break
+		} else if err != nil {
+			log.Printf("client error: %v", err)
+			break
+		}
+
+		// Create a context for the request and call the
+		// handler with it.  Set a write deadline on the
+		// connection to allow the full RequestTimeout time to
+		// generate the response.
+		nc.SetWriteDeadline(time.Now().Add(l.RequestTimeout + l.WriteTimeout))
+		ctx, cancel := context.WithTimeout(context.Background(), l.RequestTimeout)
+		err = l.handler.ServeLine(ctx, rw, line)
+		cancel()
+
+		// Close the connection on error, or on empty response.
+		if err != nil {
+			if err != ErrCloseConnection {
+				log.Printf("request error: %v", err)
+			}
+			break
+		}
+	}
+}
diff --git a/vendor/github.com/coreos/go-systemd/LICENSE b/vendor/github.com/coreos/go-systemd/LICENSE
new file mode 100644
index 0000000000000000000000000000000000000000..37ec93a14fdcd0d6e525d97c0cfa6b314eaa98d8
--- /dev/null
+++ b/vendor/github.com/coreos/go-systemd/LICENSE
@@ -0,0 +1,191 @@
+Apache License
+Version 2.0, January 2004
+http://www.apache.org/licenses/
+
+TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+1. Definitions.
+
+"License" shall mean the terms and conditions for use, reproduction, and
+distribution as defined by Sections 1 through 9 of this document.
+
+"Licensor" shall mean the copyright owner or entity authorized by the copyright
+owner that is granting the License.
+
+"Legal Entity" shall mean the union of the acting entity and all other entities
+that control, are controlled by, or are under common control with that entity.
+For the purposes of this definition, "control" means (i) the power, direct or
+indirect, to cause the direction or management of such entity, whether by
+contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the
+outstanding shares, or (iii) beneficial ownership of such entity.
+
+"You" (or "Your") shall mean an individual or Legal Entity exercising
+permissions granted by this License.
+
+"Source" form shall mean the preferred form for making modifications, including
+but not limited to software source code, documentation source, and configuration
+files.
+
+"Object" form shall mean any form resulting from mechanical transformation or
+translation of a Source form, including but not limited to compiled object code,
+generated documentation, and conversions to other media types.
+
+"Work" shall mean the work of authorship, whether in Source or Object form, made
+available under the License, as indicated by a copyright notice that is included
+in or attached to the work (an example is provided in the Appendix below).
+
+"Derivative Works" shall mean any work, whether in Source or Object form, that
+is based on (or derived from) the Work and for which the editorial revisions,
+annotations, elaborations, or other modifications represent, as a whole, an
+original work of authorship. For the purposes of this License, Derivative Works
+shall not include works that remain separable from, or merely link (or bind by
+name) to the interfaces of, the Work and Derivative Works thereof.
+
+"Contribution" shall mean any work of authorship, including the original version
+of the Work and any modifications or additions to that Work or Derivative Works
+thereof, that is intentionally submitted to Licensor for inclusion in the Work
+by the copyright owner or by an individual or Legal Entity authorized to submit
+on behalf of the copyright owner. For the purposes of this definition,
+"submitted" means any form of electronic, verbal, or written communication sent
+to the Licensor or its representatives, including but not limited to
+communication on electronic mailing lists, source code control systems, and
+issue tracking systems that are managed by, or on behalf of, the Licensor for
+the purpose of discussing and improving the Work, but excluding communication
+that is conspicuously marked or otherwise designated in writing by the copyright
+owner as "Not a Contribution."
+
+"Contributor" shall mean Licensor and any individual or Legal Entity on behalf
+of whom a Contribution has been received by Licensor and subsequently
+incorporated within the Work.
+
+2. Grant of Copyright License.
+
+Subject to the terms and conditions of this License, each Contributor hereby
+grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
+irrevocable copyright license to reproduce, prepare Derivative Works of,
+publicly display, publicly perform, sublicense, and distribute the Work and such
+Derivative Works in Source or Object form.
+
+3. Grant of Patent License.
+
+Subject to the terms and conditions of this License, each Contributor hereby
+grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
+irrevocable (except as stated in this section) patent license to make, have
+made, use, offer to sell, sell, import, and otherwise transfer the Work, where
+such license applies only to those patent claims licensable by such Contributor
+that are necessarily infringed by their Contribution(s) alone or by combination
+of their Contribution(s) with the Work to which such Contribution(s) was
+submitted. If You institute patent litigation against any entity (including a
+cross-claim or counterclaim in a lawsuit) alleging that the Work or a
+Contribution incorporated within the Work constitutes direct or contributory
+patent infringement, then any patent licenses granted to You under this License
+for that Work shall terminate as of the date such litigation is filed.
+
+4. Redistribution.
+
+You may reproduce and distribute copies of the Work or Derivative Works thereof
+in any medium, with or without modifications, and in Source or Object form,
+provided that You meet the following conditions:
+
+You must give any other recipients of the Work or Derivative Works a copy of
+this License; and
+You must cause any modified files to carry prominent notices stating that You
+changed the files; and
+You must retain, in the Source form of any Derivative Works that You distribute,
+all copyright, patent, trademark, and attribution notices from the Source form
+of the Work, excluding those notices that do not pertain to any part of the
+Derivative Works; and
+If the Work includes a "NOTICE" text file as part of its distribution, then any
+Derivative Works that You distribute must include a readable copy of the
+attribution notices contained within such NOTICE file, excluding those notices
+that do not pertain to any part of the Derivative Works, in at least one of the
+following places: within a NOTICE text file distributed as part of the
+Derivative Works; within the Source form or documentation, if provided along
+with the Derivative Works; or, within a display generated by the Derivative
+Works, if and wherever such third-party notices normally appear. The contents of
+the NOTICE file are for informational purposes only and do not modify the
+License. You may add Your own attribution notices within Derivative Works that
+You distribute, alongside or as an addendum to the NOTICE text from the Work,
+provided that such additional attribution notices cannot be construed as
+modifying the License.
+You may add Your own copyright statement to Your modifications and may provide
+additional or different license terms and conditions for use, reproduction, or
+distribution of Your modifications, or for any such Derivative Works as a whole,
+provided Your use, reproduction, and distribution of the Work otherwise complies
+with the conditions stated in this License.
+
+5. Submission of Contributions.
+
+Unless You explicitly state otherwise, any Contribution intentionally submitted
+for inclusion in the Work by You to the Licensor shall be under the terms and
+conditions of this License, without any additional terms or conditions.
+Notwithstanding the above, nothing herein shall supersede or modify the terms of
+any separate license agreement you may have executed with Licensor regarding
+such Contributions.
+
+6. Trademarks.
+
+This License does not grant permission to use the trade names, trademarks,
+service marks, or product names of the Licensor, except as required for
+reasonable and customary use in describing the origin of the Work and
+reproducing the content of the NOTICE file.
+
+7. Disclaimer of Warranty.
+
+Unless required by applicable law or agreed to in writing, Licensor provides the
+Work (and each Contributor provides its Contributions) on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied,
+including, without limitation, any warranties or conditions of TITLE,
+NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are
+solely responsible for determining the appropriateness of using or
+redistributing the Work and assume any risks associated with Your exercise of
+permissions under this License.
+
+8. Limitation of Liability.
+
+In no event and under no legal theory, whether in tort (including negligence),
+contract, or otherwise, unless required by applicable law (such as deliberate
+and grossly negligent acts) or agreed to in writing, shall any Contributor be
+liable to You for damages, including any direct, indirect, special, incidental,
+or consequential damages of any character arising as a result of this License or
+out of the use or inability to use the Work (including but not limited to
+damages for loss of goodwill, work stoppage, computer failure or malfunction, or
+any and all other commercial damages or losses), even if such Contributor has
+been advised of the possibility of such damages.
+
+9. Accepting Warranty or Additional Liability.
+
+While redistributing the Work or Derivative Works thereof, You may choose to
+offer, and charge a fee for, acceptance of support, warranty, indemnity, or
+other liability obligations and/or rights consistent with this License. However,
+in accepting such obligations, You may act only on Your own behalf and on Your
+sole responsibility, not on behalf of any other Contributor, and only if You
+agree to indemnify, defend, and hold each Contributor harmless for any liability
+incurred by, or claims asserted against, such Contributor by reason of your
+accepting any such warranty or additional liability.
+
+END OF TERMS AND CONDITIONS
+
+APPENDIX: How to apply the Apache License to your work
+
+To apply the Apache License to your work, attach the following boilerplate
+notice, with the fields enclosed by brackets "[]" replaced with your own
+identifying information. (Don't include the brackets!) The text should be
+enclosed in the appropriate comment syntax for the file format. We also
+recommend that a file or class name and description of purpose be included on
+the same "printed page" as the copyright notice for easier identification within
+third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   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/vendor/github.com/coreos/go-systemd/activation/files.go b/vendor/github.com/coreos/go-systemd/activation/files.go
new file mode 100644
index 0000000000000000000000000000000000000000..c8e85fcd58894494b84693a9840e9c820558fce6
--- /dev/null
+++ b/vendor/github.com/coreos/go-systemd/activation/files.go
@@ -0,0 +1,52 @@
+// Copyright 2015 CoreOS, Inc.
+//
+// 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.
+
+// Package activation implements primitives for systemd socket activation.
+package activation
+
+import (
+	"os"
+	"strconv"
+	"syscall"
+)
+
+// based on: https://gist.github.com/alberts/4640792
+const (
+	listenFdsStart = 3
+)
+
+func Files(unsetEnv bool) []*os.File {
+	if unsetEnv {
+		defer os.Unsetenv("LISTEN_PID")
+		defer os.Unsetenv("LISTEN_FDS")
+	}
+
+	pid, err := strconv.Atoi(os.Getenv("LISTEN_PID"))
+	if err != nil || pid != os.Getpid() {
+		return nil
+	}
+
+	nfds, err := strconv.Atoi(os.Getenv("LISTEN_FDS"))
+	if err != nil || nfds == 0 {
+		return nil
+	}
+
+	files := make([]*os.File, 0, nfds)
+	for fd := listenFdsStart; fd < listenFdsStart+nfds; fd++ {
+		syscall.CloseOnExec(fd)
+		files = append(files, os.NewFile(uintptr(fd), "LISTEN_FD_"+strconv.Itoa(fd)))
+	}
+
+	return files
+}
diff --git a/vendor/github.com/coreos/go-systemd/activation/listeners.go b/vendor/github.com/coreos/go-systemd/activation/listeners.go
new file mode 100644
index 0000000000000000000000000000000000000000..fd5dfc709c77e00796ade8fb89ca089eeb7bbc9f
--- /dev/null
+++ b/vendor/github.com/coreos/go-systemd/activation/listeners.go
@@ -0,0 +1,60 @@
+// Copyright 2015 CoreOS, Inc.
+//
+// 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.
+
+package activation
+
+import (
+	"crypto/tls"
+	"net"
+)
+
+// Listeners returns a slice containing a net.Listener for each matching socket type
+// passed to this process.
+//
+// The order of the file descriptors is preserved in the returned slice.
+// Nil values are used to fill any gaps. For example if systemd were to return file descriptors
+// corresponding with "udp, tcp, tcp", then the slice would contain {nil, net.Listener, net.Listener}
+func Listeners(unsetEnv bool) ([]net.Listener, error) {
+	files := Files(unsetEnv)
+	listeners := make([]net.Listener, len(files))
+
+	for i, f := range files {
+		if pc, err := net.FileListener(f); err == nil {
+			listeners[i] = pc
+		}
+	}
+	return listeners, nil
+}
+
+// TLSListeners returns a slice containing a net.listener for each matching TCP socket type
+// passed to this process.
+// It uses default Listeners func and forces TCP sockets handlers to use TLS based on tlsConfig.
+func TLSListeners(unsetEnv bool, tlsConfig *tls.Config) ([]net.Listener, error) {
+	listeners, err := Listeners(unsetEnv)
+
+	if listeners == nil || err != nil {
+		return nil, err
+	}
+
+	if tlsConfig != nil && err == nil {
+		for i, l := range listeners {
+			// Activate TLS only for TCP sockets
+			if l.Addr().Network() == "tcp" {
+				listeners[i] = tls.NewListener(l, tlsConfig)
+			}
+		}
+	}
+
+	return listeners, err
+}
diff --git a/vendor/github.com/coreos/go-systemd/activation/packetconns.go b/vendor/github.com/coreos/go-systemd/activation/packetconns.go
new file mode 100644
index 0000000000000000000000000000000000000000..48b2ca029df208f20fd465f510a3c9a68e6eed75
--- /dev/null
+++ b/vendor/github.com/coreos/go-systemd/activation/packetconns.go
@@ -0,0 +1,37 @@
+// Copyright 2015 CoreOS, Inc.
+//
+// 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.
+
+package activation
+
+import (
+	"net"
+)
+
+// PacketConns returns a slice containing a net.PacketConn for each matching socket type
+// passed to this process.
+//
+// The order of the file descriptors is preserved in the returned slice.
+// Nil values are used to fill any gaps. For example if systemd were to return file descriptors
+// corresponding with "udp, tcp, udp", then the slice would contain {net.PacketConn, nil, net.PacketConn}
+func PacketConns(unsetEnv bool) ([]net.PacketConn, error) {
+	files := Files(unsetEnv)
+	conns := make([]net.PacketConn, len(files))
+
+	for i, f := range files {
+		if pc, err := net.FilePacketConn(f); err == nil {
+			conns[i] = pc
+		}
+	}
+	return conns, nil
+}
diff --git a/vendor/github.com/coreos/go-systemd/daemon/sdnotify.go b/vendor/github.com/coreos/go-systemd/daemon/sdnotify.go
new file mode 100644
index 0000000000000000000000000000000000000000..ba6d41d85baae74d59958304bef2f3b8cc27fb00
--- /dev/null
+++ b/vendor/github.com/coreos/go-systemd/daemon/sdnotify.go
@@ -0,0 +1,63 @@
+// Copyright 2014 Docker, Inc.
+//
+// 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.
+//
+
+// Code forked from Docker project
+package daemon
+
+import (
+	"net"
+	"os"
+)
+
+// SdNotify sends a message to the init daemon. It is common to ignore the error.
+// If `unsetEnvironment` is true, the environment variable `NOTIFY_SOCKET`
+// will be unconditionally unset.
+//
+// It returns one of the following:
+// (false, nil) - notification not supported (i.e. NOTIFY_SOCKET is unset)
+// (false, err) - notification supported, but failure happened (e.g. error connecting to NOTIFY_SOCKET or while sending data)
+// (true, nil) - notification supported, data has been sent
+func SdNotify(unsetEnvironment bool, state string) (sent bool, err error) {
+	socketAddr := &net.UnixAddr{
+		Name: os.Getenv("NOTIFY_SOCKET"),
+		Net:  "unixgram",
+	}
+
+	// NOTIFY_SOCKET not set
+	if socketAddr.Name == "" {
+		return false, nil
+	}
+
+	if unsetEnvironment {
+		err = os.Unsetenv("NOTIFY_SOCKET")
+	}
+	if err != nil {
+		return false, err
+	}
+
+	conn, err := net.DialUnix(socketAddr.Net, nil, socketAddr)
+	// Error connecting to NOTIFY_SOCKET
+	if err != nil {
+		return false, err
+	}
+	defer conn.Close()
+
+	_, err = conn.Write([]byte(state))
+	// Error sending the message
+	if err != nil {
+		return false, err
+	}
+	return true, nil
+}
diff --git a/vendor/github.com/coreos/go-systemd/daemon/watchdog.go b/vendor/github.com/coreos/go-systemd/daemon/watchdog.go
new file mode 100644
index 0000000000000000000000000000000000000000..35a92e6e67dbf6d5900f47af95774bdf74b41a54
--- /dev/null
+++ b/vendor/github.com/coreos/go-systemd/daemon/watchdog.go
@@ -0,0 +1,72 @@
+// Copyright 2016 CoreOS, Inc.
+//
+// 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.
+
+package daemon
+
+import (
+	"fmt"
+	"os"
+	"strconv"
+	"time"
+)
+
+// SdWatchdogEnabled return watchdog information for a service.
+// Process should send daemon.SdNotify("WATCHDOG=1") every time / 2.
+// If `unsetEnvironment` is true, the environment variables `WATCHDOG_USEC`
+// and `WATCHDOG_PID` will be unconditionally unset.
+//
+// It returns one of the following:
+// (0, nil) - watchdog isn't enabled or we aren't the watched PID.
+// (0, err) - an error happened (e.g. error converting time).
+// (time, nil) - watchdog is enabled and we can send ping.
+//   time is delay before inactive service will be killed.
+func SdWatchdogEnabled(unsetEnvironment bool) (time.Duration, error) {
+	wusec := os.Getenv("WATCHDOG_USEC")
+	wpid := os.Getenv("WATCHDOG_PID")
+	if unsetEnvironment {
+		wusecErr := os.Unsetenv("WATCHDOG_USEC")
+		wpidErr := os.Unsetenv("WATCHDOG_PID")
+		if wusecErr != nil {
+			return 0, wusecErr
+		}
+		if wpidErr != nil {
+			return 0, wpidErr
+		}
+	}
+
+	if wusec == "" {
+		return 0, nil
+	}
+	s, err := strconv.Atoi(wusec)
+	if err != nil {
+		return 0, fmt.Errorf("error converting WATCHDOG_USEC: %s", err)
+	}
+	if s <= 0 {
+		return 0, fmt.Errorf("error WATCHDOG_USEC must be a positive number")
+	}
+	interval := time.Duration(s) * time.Microsecond
+
+	if wpid == "" {
+		return interval, nil
+	}
+	p, err := strconv.Atoi(wpid)
+	if err != nil {
+		return 0, fmt.Errorf("error converting WATCHDOG_PID: %s", err)
+	}
+	if os.Getpid() != p {
+		return 0, nil
+	}
+
+	return interval, nil
+}
diff --git a/vendor/github.com/theckman/go-flock/LICENSE b/vendor/github.com/theckman/go-flock/LICENSE
new file mode 100644
index 0000000000000000000000000000000000000000..aff7d358e246cb73066a17451c6017b713d5871a
--- /dev/null
+++ b/vendor/github.com/theckman/go-flock/LICENSE
@@ -0,0 +1,27 @@
+Copyright (c) 2015, Tim Heckman
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+* Redistributions of source code must retain the above copyright notice, this
+  list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright notice,
+  this list of conditions and the following disclaimer in the documentation
+  and/or other materials provided with the distribution.
+
+* Neither the name of linode-netint nor the names of its
+  contributors may be used to endorse or promote products derived from
+  this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/vendor/github.com/theckman/go-flock/README.md b/vendor/github.com/theckman/go-flock/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..82069971770bc80a663795e1e19bf7fcb56f5a56
--- /dev/null
+++ b/vendor/github.com/theckman/go-flock/README.md
@@ -0,0 +1,36 @@
+# go-flock
+[![TravisCI Build Status](https://img.shields.io/travis/theckman/go-flock/master.svg?style=flat)](https://travis-ci.org/theckman/go-flock)
+[![GoDoc](https://img.shields.io/badge/godoc-go--flock-blue.svg?style=flat)](https://godoc.org/github.com/theckman/go-flock)
+[![License](https://img.shields.io/badge/license-BSD_3--Clause-brightgreen.svg?style=flat)](https://github.com/theckman/go-flock/blob/master/LICENSE)
+
+`flock` implements a thread-safe sync.Locker interface for file locking. It also
+includes a non-blocking TryLock() function to allow locking without blocking execution.
+
+## License
+`flock` is released under the BSD 3-Clause License. See the `LICENSE` file for more details.
+
+## Intsallation
+```
+go get -u github.com/theckman/go-flock
+```
+
+## Usage
+```Go
+import "github.com/theckman/go-flock"
+
+fileLock := flock.NewFlock("/var/lock/go-lock.lock")
+
+locked, err := fileLock.TryLock()
+
+if err != nil {
+	// handle locking error
+}
+
+if locked {
+	// do work
+	fileLock.Unlock()
+}
+```
+
+For more detailed usage information take a look at the package API docs on
+[GoDoc](https://godoc.org/github.com/theckman/go-flock).
diff --git a/vendor/github.com/theckman/go-flock/flock.go b/vendor/github.com/theckman/go-flock/flock.go
new file mode 100644
index 0000000000000000000000000000000000000000..2582077ffc67ecde4d84f37c59f6594dccedd99d
--- /dev/null
+++ b/vendor/github.com/theckman/go-flock/flock.go
@@ -0,0 +1,61 @@
+// Copyright 2015 Tim Heckman. All rights reserved.
+// Use of this source code is governed by the BSD 3-Clause
+// license that can be found in the LICENSE file.
+
+// Package flock implements a thread-safe sync.Locker interface for file locking.
+// It also includes a non-blocking TryLock() function to allow locking
+// without blocking execution.
+//
+// Package flock is released under the BSD 3-Clause License. See the LICENSE file
+// for more details.
+package flock
+
+import (
+	"os"
+	"sync"
+)
+
+// Flock is the struct type to handle file locking. All fields are unexported,
+// with access to some of the fields provided by getter methods (Path() and Locked()).
+type Flock struct {
+	path string
+	m    sync.RWMutex
+	fh   *os.File
+	l    bool
+}
+
+// NewFlock is a function to return a new instance of *Flock. The only parameter
+// it takes is the path to the desired lockfile.
+func NewFlock(path string) *Flock {
+	return &Flock{path: path}
+}
+
+// Path is a function to return the path as provided in NewFlock().
+func (f *Flock) Path() string {
+	return f.path
+}
+
+// Locked is a function to return the current lock state (locked: true, unlocked: false).
+func (f *Flock) Locked() bool {
+	f.m.RLock()
+	defer f.m.RUnlock()
+	return f.l
+}
+
+func (f *Flock) String() string {
+	return f.path
+}
+
+func (f *Flock) setFh() error {
+	// open a new os.File instance
+	// create it if it doesn't exist, truncate it if it does exist, open the file read-write
+	fh, err := os.OpenFile(f.path, os.O_CREATE|os.O_TRUNC|os.O_RDWR, os.FileMode(0600))
+
+	if err != nil {
+		return err
+	}
+
+	// set the filehandle on the struct
+	f.fh = fh
+	return nil
+}
diff --git a/vendor/github.com/theckman/go-flock/flock_unix.go b/vendor/github.com/theckman/go-flock/flock_unix.go
new file mode 100644
index 0000000000000000000000000000000000000000..8c9a64f0732fed55a5300f6314bb48abebf13a5e
--- /dev/null
+++ b/vendor/github.com/theckman/go-flock/flock_unix.go
@@ -0,0 +1,104 @@
+// Copyright 2015 Tim Heckman. All rights reserved.
+// Use of this source code is governed by the BSD 3-Clause
+// license that can be found in the LICENSE file.
+
+// +build !windows
+
+package flock
+
+import (
+	"syscall"
+)
+
+// Lock is a blocking call to try and take the file lock. It will wait until it
+// is able to obtain the exclusive file lock. It's recommended that TryLock() be
+// used over this function. This function may block the ability to query the
+// current Locked() status due to a RW-mutex lock.
+//
+// If we are already locked, this function short-circuits and returns immediately
+// assuming it can take the mutex lock.
+func (f *Flock) Lock() error {
+	f.m.Lock()
+	defer f.m.Unlock()
+
+	if f.l {
+		return nil
+	}
+
+	if f.fh == nil {
+		if err := f.setFh(); err != nil {
+			return err
+		}
+	}
+
+	if err := syscall.Flock(int(f.fh.Fd()), syscall.LOCK_EX); err != nil {
+		return err
+	}
+
+	f.l = true
+	return nil
+}
+
+// Unlock is a function to unlock the file. This file takes a RW-mutex lock, so
+// while it is running the Locked() function will be blocked.
+//
+// This function short-circuits if we are unlocked already. If not, it calls
+// syscall.LOCK_UN on the file and closes the file descriptor It does not remove
+// the file from disk. It's up to your application to do.
+func (f *Flock) Unlock() error {
+	f.m.Lock()
+	defer f.m.Unlock()
+
+	// if we aren't locked or if the lockfile instance is nil
+	// just return a nil error because we are unlocked
+	if !f.l || f.fh == nil {
+		return nil
+	}
+
+	// mark the file as unlocked
+	if err := syscall.Flock(int(f.fh.Fd()), syscall.LOCK_UN); err != nil {
+		return err
+	}
+
+	f.fh.Close()
+
+	f.l = false
+	f.fh = nil
+
+	return nil
+}
+
+// TryLock is the preferred function for taking a file lock. This function does
+// take a RW-mutex lock before it tries to lock the file, so there is the
+// possibility that this function may block for a short time if another goroutine
+// is trying to take any action.
+//
+// The actual file lock is non-blocking. If we are unable to get the exclusive
+// file lock, the function will return false instead of waiting for the lock.
+// If we get the lock, we also set the *Flock instance as being locked.
+func (f *Flock) TryLock() (bool, error) {
+	f.m.Lock()
+	defer f.m.Unlock()
+
+	if f.l {
+		return true, nil
+	}
+
+	if f.fh == nil {
+		if err := f.setFh(); err != nil {
+			return false, err
+		}
+	}
+
+	err := syscall.Flock(int(f.fh.Fd()), syscall.LOCK_EX|syscall.LOCK_NB)
+
+	switch err {
+	case syscall.EWOULDBLOCK:
+		return false, nil
+	case nil:
+		f.l = true
+		return true, nil
+	}
+
+	return false, err
+}
diff --git a/vendor/github.com/theckman/go-flock/flock_winapi.go b/vendor/github.com/theckman/go-flock/flock_winapi.go
new file mode 100644
index 0000000000000000000000000000000000000000..e4a25769a005054019801929dee4df1766f12dce
--- /dev/null
+++ b/vendor/github.com/theckman/go-flock/flock_winapi.go
@@ -0,0 +1,92 @@
+// Copyright 2015 Tim Heckman. All rights reserved.
+// Use of this source code is governed by the BSD 3-Clause
+// license that can be found in the LICENSE file.
+
+// +build windows
+
+package flock
+
+import (
+	"syscall"
+	"unsafe"
+)
+
+var (
+	kernel32, _         = syscall.LoadLibrary("kernel32.dll")
+	procLockFileEx, _   = syscall.GetProcAddress(kernel32, "LockFileEx")
+	procUnlockFileEx, _ = syscall.GetProcAddress(kernel32, "UnlockFileEx")
+)
+
+const (
+	LOCKFILE_FAIL_IMMEDIATELY = 0x00000001
+	LOCKFILE_EXCLUSIVE_LOCK   = 0x00000002
+)
+
+// Do the interface allocations only once for common
+// Errno values.
+const (
+	errnoERROR_IO_PENDING = 997
+)
+
+var (
+	errERROR_IO_PENDING error = syscall.Errno(errnoERROR_IO_PENDING)
+)
+
+// errnoErr returns common boxed Errno values, to prevent
+// allocations at runtime.
+func errnoErr(e syscall.Errno) error {
+	switch e {
+	case 0:
+		return nil
+	case errnoERROR_IO_PENDING:
+		return errERROR_IO_PENDING
+	}
+	// TODO: add more here, after collecting data on the common
+	// error values see on Windows. (perhaps when running
+	// all.bat?)
+	return e
+}
+
+func lockFileEx(handle syscall.Handle, flags uint32, reserved uint32, numberOfBytesToLockLow uint32, numberOfBytesToLockHigh uint32, offset *syscall.Overlapped) (success bool, err error) {
+	r1, _, e1 := syscall.Syscall6(
+		uintptr(procLockFileEx),
+		6,
+		uintptr(handle),
+		uintptr(flags),
+		uintptr(reserved),
+		uintptr(numberOfBytesToLockLow),
+		uintptr(numberOfBytesToLockHigh),
+		uintptr(unsafe.Pointer(offset)))
+
+	success = r1 == 1
+	if !success {
+		if e1 != 0 {
+			err = errnoErr(e1)
+		} else {
+			err = syscall.EINVAL
+		}
+	}
+	return
+}
+
+func unlockFileEx(handle syscall.Handle, reserved uint32, numberOfBytesToLockLow uint32, numberOfBytesToLockHigh uint32, offset *syscall.Overlapped) (success bool, err error) {
+	r1, _, e1 := syscall.Syscall6(
+		uintptr(procUnlockFileEx),
+		5,
+		uintptr(handle),
+		uintptr(reserved),
+		uintptr(numberOfBytesToLockLow),
+		uintptr(numberOfBytesToLockHigh),
+		uintptr(unsafe.Pointer(offset)),
+		0)
+
+	success = r1 == 1
+	if !success {
+		if e1 != 0 {
+			err = errnoErr(e1)
+		} else {
+			err = syscall.EINVAL
+		}
+	}
+	return
+}
diff --git a/vendor/github.com/theckman/go-flock/flock_windows.go b/vendor/github.com/theckman/go-flock/flock_windows.go
new file mode 100644
index 0000000000000000000000000000000000000000..c3a04e9614291fcd3deb8deba50b4a7f2f36fe1a
--- /dev/null
+++ b/vendor/github.com/theckman/go-flock/flock_windows.go
@@ -0,0 +1,100 @@
+// Copyright 2015 Tim Heckman. All rights reserved.
+// Use of this source code is governed by the BSD 3-Clause
+// license that can be found in the LICENSE file.
+
+package flock
+
+import (
+	"syscall"
+)
+
+// Lock is a blocking call to try and take the file lock. It will wait until it
+// is able to obtain the exclusive file lock. It's recommended that TryLock() be
+// used over this function. This function may block the ability to query the
+// current Locked() status due to a RW-mutex lock.
+//
+// If we are already locked, this function short-circuits and returns immediately
+// assuming it can take the mutex lock.
+func (f *Flock) Lock() error {
+	f.m.Lock()
+	defer f.m.Unlock()
+
+	if f.l {
+		return nil
+	}
+
+	if f.fh == nil {
+		if err := f.setFh(); err != nil {
+			return err
+		}
+	}
+
+	if _, err := lockFileEx(syscall.Handle(f.fh.Fd()), LOCKFILE_EXCLUSIVE_LOCK, 0, 1, 0, &syscall.Overlapped{}); err != nil {
+		return err
+	}
+
+	f.l = true
+	return nil
+}
+
+// Unlock is a function to unlock the file. This file takes a RW-mutex lock, so
+// while it is running the Locked() function will be blocked.
+//
+// This function short-circuits if we are unlocked already. If not, it calls
+// syscall.LOCK_UN on the file and closes the file descriptor It does not remove
+// the file from disk. It's up to your application to do.
+func (f *Flock) Unlock() error {
+	f.m.Lock()
+	defer f.m.Unlock()
+
+	// if we aren't locked or if the lockfile instance is nil
+	// just return a nil error because we are unlocked
+	if !f.l || f.fh == nil {
+		return nil
+	}
+
+	// mark the file as unlocked
+	if _, err := unlockFileEx(syscall.Handle(f.fh.Fd()), 0, 1, 0, &syscall.Overlapped{}); err != nil {
+		return err
+	}
+
+	f.fh.Close()
+
+	f.l = false
+	f.fh = nil
+
+	return nil
+}
+
+// TryLock is the preferred function for taking a file lock. This function does
+// take a RW-mutex lock before it tries to lock the file, so there is the
+// possibility that this function may block for a short time if another goroutine
+// is trying to take any action.
+//
+// The actual file lock is non-blocking. If we are unable to get the exclusive
+// file lock, the function will return false instead of waiting for the lock.
+// If we get the lock, we also set the *Flock instance as being locked.
+func (f *Flock) TryLock() (bool, error) {
+	f.m.Lock()
+	defer f.m.Unlock()
+
+	if f.l {
+		return true, nil
+	}
+
+	if f.fh == nil {
+		if err := f.setFh(); err != nil {
+			return false, err
+		}
+	}
+
+	_, err := lockFileEx(syscall.Handle(f.fh.Fd()), LOCKFILE_EXCLUSIVE_LOCK|LOCKFILE_FAIL_IMMEDIATELY, 0, 1, 0, &syscall.Overlapped{})
+
+	switch err {
+	case nil:
+		f.l = true
+		return true, nil
+	}
+
+	return false, err
+}
diff --git a/vendor/vendor.json b/vendor/vendor.json
index a40d3404e1821fba6e2c0edf39f2ef4619e13307..95789792e84a7e53a7ad9644e8af74d8cbf5a200 100644
--- a/vendor/vendor.json
+++ b/vendor/vendor.json
@@ -5,26 +5,32 @@
 		{
 			"checksumSHA1": "raJx5BjBbVQG0ylGSjPpi+JvqjU=",
 			"path": "git.autistici.org/ai3/go-common",
-			"revision": "0cc062297e2c27f9a1abcb1a00172d1e0281f8cb",
-			"revisionTime": "2017-12-14T08:46:15Z"
+			"revision": "a65293114a1adbb45d047a8f9014a307ec0d9051",
+			"revisionTime": "2018-01-12T09:10:27Z"
 		},
 		{
 			"checksumSHA1": "o+rWKVQIDy79ZwrItwa5/whAL6g=",
 			"path": "git.autistici.org/ai3/go-common/clientutil",
-			"revision": "9b20acad90c411c48f7ddc837a35ef3d0d6f98d4",
-			"revisionTime": "2017-12-17T20:32:41Z"
+			"revision": "a65293114a1adbb45d047a8f9014a307ec0d9051",
+			"revisionTime": "2018-01-12T09:10:27Z"
 		},
 		{
-			"checksumSHA1": "mEnXMNziH82HFtGngHU19VHTVHs=",
+			"checksumSHA1": "iHObDrZa0HlyzdelqAaGfKNzpiM=",
 			"path": "git.autistici.org/ai3/go-common/ldap",
-			"revision": "0cc062297e2c27f9a1abcb1a00172d1e0281f8cb",
-			"revisionTime": "2017-12-14T08:46:15Z"
+			"revision": "a65293114a1adbb45d047a8f9014a307ec0d9051",
+			"revisionTime": "2018-01-12T09:10:27Z"
 		},
 		{
 			"checksumSHA1": "nlGRxexjZUxnHc/z/+ZqV/Xq51w=",
 			"path": "git.autistici.org/ai3/go-common/serverutil",
-			"revision": "aa88011352b67032c19b0c14eaa06b3417176753",
-			"revisionTime": "2017-12-19T17:20:40Z"
+			"revision": "a65293114a1adbb45d047a8f9014a307ec0d9051",
+			"revisionTime": "2018-01-12T09:10:27Z"
+		},
+		{
+			"checksumSHA1": "T2vf4xzKRqoIjfXlofMgudKA8rA=",
+			"path": "git.autistici.org/ai3/go-common/unix",
+			"revision": "a65293114a1adbb45d047a8f9014a307ec0d9051",
+			"revisionTime": "2018-01-12T09:10:27Z"
 		},
 		{
 			"checksumSHA1": "DFjm2ZJpUwioPApa3htGXLEFWl8=",
@@ -44,6 +50,18 @@
 			"revision": "309aa717adbf351e92864cbedf9cca0b769a4b5a",
 			"revisionTime": "2017-10-07T11:45:50Z"
 		},
+		{
+			"checksumSHA1": "RBwpnMpfQt7Jo7YWrRph0Vwe+f0=",
+			"path": "github.com/coreos/go-systemd/activation",
+			"revision": "d2196463941895ee908e13531a23a39feb9e1243",
+			"revisionTime": "2017-07-31T11:19:25Z"
+		},
+		{
+			"checksumSHA1": "+Zz+leZHHC9C0rx8DoRuffSRPso=",
+			"path": "github.com/coreos/go-systemd/daemon",
+			"revision": "d2196463941895ee908e13531a23a39feb9e1243",
+			"revisionTime": "2017-07-31T11:19:25Z"
+		},
 		{
 			"checksumSHA1": "yqF125xVSkmfLpIVGrLlfE05IUk=",
 			"path": "github.com/golang/protobuf/proto",
@@ -128,6 +146,12 @@
 			"revision": "a6e9df898b1336106c743392c48ee0b71f5c4efa",
 			"revisionTime": "2017-10-17T21:40:25Z"
 		},
+		{
+			"checksumSHA1": "cDiE2qLTJ2kmiC7k1KS+AbP87X8=",
+			"path": "github.com/theckman/go-flock",
+			"revision": "6de226b0d5f040ed85b88c82c381709b98277f3d",
+			"revisionTime": "2017-05-22T02:22:41Z"
+		},
 		{
 			"checksumSHA1": "X6Q8nYb+KXh+64AKHwWOOcyijHQ=",
 			"origin": "git.autistici.org/id/go-sso/vendor/golang.org/x/crypto/ed25519",