Commit Diff


commit - /dev/null
commit + 8d1b399bf516b09e00a953ce65ee1d67e4854251
blob - /dev/null
blob + 3e6292ab62fb7a49a707cf7e5bcf9b3773e9ed81 (mode 644)
--- /dev/null
+++ .gitignore
@@ -0,0 +1,24 @@
+Makefile
+Makefile.in
+aclocal.m4
+autom4te.cache
+compile
+config.h.in
+configure
+depcomp
+install-sh
+missing
+ylwrap
+stamp-h1
+config.status
+config.log
+config.h
+.deps
+
+compile_flags.txt
+
+kamid
+kamictl
+*.o
+kamid.conf
+parse.c
blob - /dev/null
blob + a1f37807e9f87083a9511dc3c4339e97fd60d74f (mode 644)
--- /dev/null
+++ LICENSE
@@ -0,0 +1,15 @@
+Copyright 2021 Omar Polo
+
+Permission to use, copy, modify, and/or distribute this software for
+any purpose with or without fee is hereby granted, provided that the
+above copyright notice and this permission notice appear in all
+copies.
+
+THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL
+WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE
+AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL
+DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR
+PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
+TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+PERFORMANCE OF THIS SOFTWARE.
blob - /dev/null
blob + 283776fb74ccd4126192e81c47f378ef88d4160d (mode 644)
--- /dev/null
+++ Makefile.am
@@ -0,0 +1,44 @@
+bin_PROGRAMS =		kamictl kamid
+
+kamictl_SOURCES =	compat.h	\
+			compat/*.[ch]	\
+			ctl_parser.c	\
+			ctl_parser.h	\
+			kamictl.c	\
+			log.c		\
+			log.h		\
+			sandbox.c	\
+			sandbox.h
+
+kamid_SOURCES =		client.c	\
+			client.h	\
+			control.c	\
+			control.h	\
+			compat.h	\
+			compat/*.[ch]	\
+			kamid.c		\
+			kamid.h		\
+			listener.c	\
+			listener.h	\
+			log.c		\
+			log.h		\
+			parse.y		\
+			sandbox.c	\
+			sandbox.h	\
+			table.c		\
+			table.h		\
+			table_static.c	\
+			utils.c		\
+			utils.h
+
+LDADD =			$(LIBOBJ)
+
+BUILT_SOURCES =		compile_flags.txt
+CLEANFILES =		compile_flags.txt
+
+dist_doc_DATA =		README.md LICENSE
+dist_man5_MANS =	kamid.conf.5
+dist_man8_MANS =	kamictl.8 kamid.8
+
+compile_flags.txt:
+	printf "%s\n" ${CFLAGS} > compile_flags.txt
blob - /dev/null
blob + a581c7e45eee806ee4efc42cfa56e70a18032bb0 (mode 644)
--- /dev/null
+++ README.md
@@ -0,0 +1,26 @@
+# kamid -- 9p file server daemon
+
+kamid is a FREE implementation of a 9p file server daemon for
+UNIX-like systems.
+
+
+## Building
+
+kamid depends on libtls, libevent and yacc/GNU bison.  To build from a
+release tarball:
+
+	./configure
+	make
+	sudo make install # eventually
+
+to build from a git checkout:
+
+	./bootstrap
+	./configure
+	make
+
+
+## License
+
+kamid is released under a BSD-like license.  The bulk of the code is
+under the ISC license, but some file are BSD2 or BSD3.
blob - /dev/null
blob + 70027cbf3cf809c67ca8b5cff38df304aa29bcf6 (mode 755)
--- /dev/null
+++ bootstrap
@@ -0,0 +1,3 @@
+#!/bin/sh
+
+exec autoreconf -vfi
blob - /dev/null
blob + a667f8059a426d01cbf9b232436418e8945da1f3 (mode 644)
--- /dev/null
+++ client.c
@@ -0,0 +1,233 @@
+/*
+ * Copyright (c) 2021 Omar Polo <op@omarpolo.com>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include "compat.h"
+
+#include <errno.h>
+#include <pwd.h>
+#include <signal.h>
+#include <stdlib.h>
+#include <string.h>
+#include <syslog.h>
+#include <unistd.h>
+
+#include "client.h"
+#include "kamid.h"
+#include "log.h"
+#include "sandbox.h"
+
+static struct imsgev	*iev_listener;
+
+static __dead void	client_shutdown(void);
+static void		client_sig_handler(int, short, void *);
+static void		client_dispatch_listener(int, short, void *);
+static void		client_privdrop(const char *, const char *);
+
+static int		client_imsg_compose_listener(int, uint32_t,
+    const void *, uint16_t);
+
+__dead void
+client(int debug, int verbose)
+{
+	struct event	ev_sigint, ev_sigterm;
+
+	log_init(debug, LOG_DAEMON);
+	log_setverbose(verbose);
+
+	setproctitle("client");
+	log_procinit("client");
+
+	log_debug("warming up");
+
+	event_init();
+
+	/* Setup signal handlers */
+	signal_set(&ev_sigint, SIGINT, client_sig_handler, NULL);
+	signal_set(&ev_sigterm, SIGTERM, client_sig_handler, NULL);
+
+	signal_add(&ev_sigint, NULL);
+	signal_add(&ev_sigterm, NULL);
+
+	signal(SIGPIPE, SIG_IGN);
+	signal(SIGHUP, SIG_IGN);
+
+	/* Setup pipe and event handler to the listener process */
+	if ((iev_listener = malloc(sizeof(*iev_listener))) == NULL)
+		fatal(NULL);
+
+	imsg_init(&iev_listener->ibuf, 3);
+	iev_listener->handler = client_dispatch_listener;
+
+	/* Setup event handlers. */
+	iev_listener->events = EV_READ;
+	event_set(&iev_listener->ev, iev_listener->ibuf.fd,
+	    iev_listener->events, iev_listener->handler, iev_listener);
+	event_add(&iev_listener->ev, NULL);
+
+	log_debug("before dispatch");
+	event_dispatch();
+	client_shutdown();
+}
+
+static __dead void
+client_shutdown(void)
+{
+	msgbuf_clear(&iev_listener->ibuf.w);
+	close(iev_listener->ibuf.fd);
+
+        free(iev_listener);
+
+	log_info("client exiting");
+	exit(0);
+}
+
+static void
+client_sig_handler(int sig, short event, void *d)
+{
+	/*
+	 * Normal signal handler rules don't apply because libevent
+	 * decouples for us.
+	 */
+
+	switch (sig) {
+	case SIGINT:
+	case SIGTERM:
+		client_shutdown();
+	default:
+		fatalx("unexpected signal %d", sig);
+	}
+}
+
+#define AUTH_NONE 0
+#define AUTH_USER 1
+#define AUTH_DONE 2
+
+static void
+client_dispatch_listener(int fd, short event, void *d)
+{
+	static int		 auth = AUTH_NONE;
+	static char		 username[64] = {0};
+	static char		 dir[PATH_MAX] = {0};
+	struct imsg		 imsg;
+	struct imsgev		*iev = d;
+	struct imsgbuf		*ibuf;
+	ssize_t			 n;
+	int			 shut = 0;
+
+	ibuf = &iev->ibuf;
+
+	if (event & EV_READ) {
+		if ((n = imsg_read(ibuf)) == -1 && errno != EAGAIN)
+			fatal("imsg_read error");
+		if (n == 0)	/* Connection closed */
+			shut = 1;
+	}
+	if (event & EV_WRITE) {
+		if ((n = msgbuf_write(&ibuf->w)) == -1 && errno != EAGAIN)
+			fatal("msgbuf_write");
+		if (n == 0)	/* Connection closed */
+			shut = 1;
+	}
+
+	for (;;) {
+		if ((n = imsg_get(ibuf, &imsg)) == -1)
+			fatal("%s: imsg_get error", __func__);
+		if (n == 0)	/* No more messages. */
+			break;
+
+		log_debug("client: got message type %d", imsg.hdr.type);
+		switch (imsg.hdr.type) {
+		case IMSG_AUTH:
+			if (auth)
+				fatalx("%s: IMSG_AUTH already done", __func__);
+			auth = AUTH_USER;
+			((char *)imsg.data)[IMSG_DATA_SIZE(imsg)-1] = '\0';
+			strlcpy(username, imsg.data, sizeof(username));
+			break;
+		case IMSG_AUTH_DIR:
+			if (auth != AUTH_USER)
+				fatalx("%s: IMSG_AUTH_DIR not after IMSG_AUTH",
+				    __func__);
+			auth = AUTH_DONE;
+			((char *)imsg.data)[IMSG_DATA_SIZE(imsg)-1] = '\0';
+			strlcpy(dir, imsg.data, sizeof(dir));
+			client_privdrop(username, dir);
+			memset(username, 0, sizeof(username));
+			memset(dir, 0, sizeof(username));
+			break;
+		case IMSG_BUF:
+			/* echo! */
+			client_imsg_compose_listener(IMSG_BUF, imsg.hdr.peerid,
+			    imsg.data, IMSG_DATA_SIZE(imsg));
+			break;
+		case IMSG_CONN_GONE:
+			log_debug("closing");
+			shut = 1;
+			break;
+		default:
+			log_debug("%s: unexpected imsg %d",
+			    __func__, imsg.hdr.type);
+			break;
+		}
+		imsg_free(&imsg);
+	}
+
+	if (!shut)
+		imsg_event_add(iev);
+	else {
+		/* This pipe is dead.  Remove its event handler. */
+		event_del(&iev->ev);
+		log_warnx("pipe closed, shutting down...");
+		event_loopexit(NULL);
+	}
+}
+
+static void
+client_privdrop(const char *username, const char *dir)
+{
+	struct passwd *pw;
+
+	setproctitle("client %s", username);
+
+	if ((pw = getpwnam(username)) == NULL)
+		fatalx("getpwnam(%s) failed", username);
+
+	if (chroot(dir) == -1)
+		fatal("chroot");
+	if (chdir("/") == -1)
+		fatal("chdir(\"/\")");
+
+	if (setgroups(1, &pw->pw_gid) ||
+	    setresgid(pw->pw_gid, pw->pw_gid, pw->pw_gid) ||
+	    setresuid(pw->pw_uid, pw->pw_uid, pw->pw_uid))
+		fatal("can't drop privileges");
+
+	sandbox_client();
+	log_debug("client ready");
+}
+
+static int
+client_imsg_compose_listener(int type, uint32_t peerid,
+    const void *data, uint16_t len)
+{
+	int ret;
+
+	if ((ret = imsg_compose(&iev_listener->ibuf, type, peerid, 0, -1,
+	    data, len)) != -1)
+		imsg_event_add(iev_listener);
+
+	return ret;
+}
blob - /dev/null
blob + a007ec7f0ca02d97660866c9201bfe28e6af56a4 (mode 644)
--- /dev/null
+++ client.h
@@ -0,0 +1,24 @@
+/*
+ * Copyright (c) 2021 Omar Polo <op@omarpolo.com>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#ifndef CLIENT_H
+#define CLIENT_H
+
+#include "compat.h"
+
+__dead void	client(int, int);
+
+#endif
blob - /dev/null
blob + 1b50f0c49503e60bd7ce9207d9cc07e7686673c2 (mode 644)
--- /dev/null
+++ compat/getprogname.c
@@ -0,0 +1,39 @@
+/*
+ * Copyright (c) 2021 Omar Polo <op@omarpolo.com>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include "compat.h"
+
+#ifdef HAVE_PROGRAM_INVOCATION_SHORT_NAME
+
+#include <errno.h>
+
+extern char *program_invocation_short_name;
+
+const char *
+getprogname(void)
+{
+	return program_invocation_short_name;
+}
+
+#else
+
+const char *
+getprogname(void)
+{
+	return "kamid";
+}
+
+#endif
blob - /dev/null
blob + 0ee7185b60aaf71dee305cd764f52643acccd3a1 (mode 644)
--- /dev/null
+++ compat/setprocname.c
@@ -0,0 +1,54 @@
+/*
+ * Copyright (c) 2016 Nicholas Marriott <nicholas.marriott@gmail.com>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF MIND, USE, DATA OR PROFITS, WHETHER
+ * IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING
+ * OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include "../compat.h"
+
+#include <sys/types.h>
+
+#include <stdarg.h>
+#include <stdio.h>
+#include <string.h>
+
+#if HAVE_PR_SET_NAME
+
+#include <sys/prctl.h>
+
+void
+setproctitle(const char *fmt, ...)
+{
+	char	title[16], name[16], *cp;
+	va_list	ap;
+	int	used;
+
+	va_start(ap, fmt);
+	vsnprintf(title, sizeof title, fmt, ap);
+	va_end(ap);
+
+	used = snprintf(name, sizeof name, "%s: %s", getprogname(), title);
+	if (used >= (int)sizeof name) {
+		cp = strrchr(name, ' ');
+		if (cp != NULL)
+			*cp = '\0';
+	}
+	prctl(PR_SET_NAME, name);
+}
+#else
+void
+setproctitle(const char *fmt, ...)
+{
+	(void)fmt;
+}
+#endif
blob - /dev/null
blob + b114fceb83c0e6f8c970e6773504d2f659d179bd (mode 644)
--- /dev/null
+++ compat.h
@@ -0,0 +1,54 @@
+/*
+ * Copyright (c) 2021 Omar Polo <op@omarpolo.com>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#ifndef COMPAT_H
+#define COMPAT_H
+
+#include "config.h"
+
+#include <sys/types.h>
+#include <sys/uio.h>
+
+#include <stdarg.h>
+#include <stddef.h>
+#include <stdint.h>
+
+#ifdef HAVE_QUEUE_H
+# include <sys/queue.h>
+#else
+# include "compat/queue.h"
+#endif
+
+#ifdef HAVE_LIBUTIL
+# include <imsg.h>
+# include <ohash.h>
+# include <util.h>
+#else
+# include "compat/imsg.h"
+# include "compat/ohash.h"
+# define FMT_SCALED_STRSIZE      7       /* minus sign, 4 digits, suffix, null byte */
+int		 fmt_scaled(long long, char *);
+#endif
+
+#ifndef HAVE_GETPROGNAME
+const char	*getprogname(void);
+#endif
+
+#ifndef HAVE_SETPROGNAME
+void		 setproctitle(const char *, ...);
+#endif
+
+#endif
blob - /dev/null
blob + dafd12868478445d7aa9b8d670211df2247e16db (mode 644)
--- /dev/null
+++ configure.ac
@@ -0,0 +1,70 @@
+AC_INIT([kamid], [0.1], [kamid@omarpolo.com], [kamid], [gemini://kamid.omarpolo.com])
+AC_CONFIG_LIBOBJ_DIR(compat)
+AM_INIT_AUTOMAKE([-Wall foreign subdir-objects])
+AC_PROG_CC
+AC_USE_SYSTEM_EXTENSIONS
+AC_PROG_YACC
+
+AC_REPLACE_FUNCS([
+	getprogname	\
+	setprogname	\
+])
+
+AC_MSG_CHECKING([for sys/queue.h with TAILQ_FOREACH_SAFE])
+AC_COMPILE_IFELSE([AC_LANG_PROGRAM([
+#include <sys/queue.h>
+#include <stddef.h>
+], [
+	TAILQ_HEAD(tailhead, entry) head;
+	struct entry {
+		TAILQ_ENTRY(entry) entries;
+	} *np, *nt;
+	TAILQ_INIT(&head);
+	TAILQ_FOREACH_SAFE(np, &head, entries, nt) {
+		/* nop */ ;
+	}
+	return 0;
+])], [
+	AC_MSG_RESULT(yes)
+	AC_DEFINE([HAVE_QUEUE_H], 1, [QUEUE_H])
+], AC_MSG_RESULT(no))
+
+AC_CHECK_DECL(PR_SET_NAME, AC_DEFINE([HAVE_PR_SET_NAME], 1, [pr_set_name]), [],
+	[[#include <sys/prctl.h>]])
+
+AC_CHECK_LIB(tls, tls_init, [], [
+	AC_MSG_ERROR([requires libtls])
+])
+
+AC_CHECK_LIB(event, event_init, [], [
+	AC_MSG_ERROR([requires libevent])
+])
+
+AC_CHECK_LIB(util, imsg_init, [], [
+	AC_LIBOBJ(imsg)
+	AC_LIBOBJ(imsg-buffer)
+])
+
+# check compiler flags
+AC_DEFUN([CC_ADD_CHECK_FLAGS], [
+	AC_MSG_CHECKING([if $CC supports $1 flag])
+	old_CFLAGS="$CFLAGS"
+	CFLAGS="$CFLAGS $1"
+	AC_COMPILE_IFELSE([AC_LANG_PROGRAM([],[])],
+		AC_MSG_RESULT(yes),
+		AC_MSG_RESULT(no)
+		CFLAGS="$old_CFLAGS")
+])
+CC_ADD_CHECK_FLAGS([-Wall])
+CC_ADD_CHECK_FLAGS([-Wextra])
+CC_ADD_CHECK_FLAGS([-Wmissing-prototypes])
+CC_ADD_CHECK_FLAGS([-Wstrict-prototypes])
+CC_ADD_CHECK_FLAGS([-Wwrite-strings])
+CC_ADD_CHECK_FLAGS([-Wno-unused-parameter])
+
+AC_CONFIG_HEADERS([config.h])
+AC_CONFIG_FILES([
+	Makefile
+])
+
+AC_OUTPUT
blob - /dev/null
blob + 380aa0cd20327bab3abc65db7f51690a8f5fa4d5 (mode 644)
--- /dev/null
+++ control.c
@@ -0,0 +1,289 @@
+/*
+ * Copyright (c) 2003, 2004 Henning Brauer <henning@openbsd.org>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <sys/types.h>
+#include <sys/queue.h>
+#include <sys/stat.h>
+#include <sys/socket.h>
+#include <sys/uio.h>
+#include <sys/un.h>
+
+#include <netinet/in.h>
+#include <net/if.h>
+
+#include <errno.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include "log.h"
+#include "kamid.h"
+#include "control.h"
+#include "listener.h"
+
+#define	CONTROL_BACKLOG	5
+
+struct {
+	struct event	ev;
+	struct event	evt;
+	int		fd;
+} control_state = {.fd = -1};
+
+struct ctl_conn {
+	TAILQ_ENTRY(ctl_conn)	entry;
+	struct imsgev		iev;
+};
+
+TAILQ_HEAD(ctl_conns, ctl_conn)	ctl_conns = TAILQ_HEAD_INITIALIZER(ctl_conns);
+
+struct ctl_conn	*control_connbyfd(int);
+struct ctl_conn	*control_connbypid(pid_t);
+void		 control_close(int);
+
+int
+control_init(const char *path)
+{
+	struct sockaddr_un	 sun;
+	int			 fd;
+	mode_t			 old_umask;
+
+	if ((fd = socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC | SOCK_NONBLOCK,
+	    0)) == -1) {
+		log_warn("%s: socket", __func__);
+		return (-1);
+	}
+
+	memset(&sun, 0, sizeof(sun));
+	sun.sun_family = AF_UNIX;
+	strlcpy(sun.sun_path, path, sizeof(sun.sun_path));
+
+	if (unlink(path) == -1)
+		if (errno != ENOENT) {
+			log_warn("%s: unlink %s", __func__, path);
+			close(fd);
+			return (-1);
+		}
+
+	old_umask = umask(S_IXUSR|S_IXGRP|S_IWOTH|S_IROTH|S_IXOTH);
+	if (bind(fd, (struct sockaddr *)&sun, sizeof(sun)) == -1) {
+		log_warn("%s: bind: %s", __func__, path);
+		close(fd);
+		umask(old_umask);
+		return (-1);
+	}
+	umask(old_umask);
+
+	if (chmod(path, S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP) == -1) {
+		log_warn("%s: chmod", __func__);
+		close(fd);
+		(void)unlink(path);
+		return (-1);
+	}
+
+	return (fd);
+}
+
+int
+control_listen(int fd)
+{
+	if (control_state.fd != -1)
+		fatalx("%s: received unexpected controlsock", __func__);
+
+	control_state.fd = fd;
+	if (listen(control_state.fd, CONTROL_BACKLOG) == -1) {
+		log_warn("%s: listen", __func__);
+		return (-1);
+	}
+
+	event_set(&control_state.ev, control_state.fd, EV_READ,
+	    control_accept, NULL);
+	event_add(&control_state.ev, NULL);
+	evtimer_set(&control_state.evt, control_accept, NULL);
+
+	return (0);
+}
+
+void
+control_accept(int listenfd, short event, void *bula)
+{
+	int			 connfd;
+	socklen_t		 len;
+	struct sockaddr_un	 sun;
+	struct ctl_conn		*c;
+
+	event_add(&control_state.ev, NULL);
+	if ((event & EV_TIMEOUT))
+		return;
+
+	len = sizeof(sun);
+	if ((connfd = accept4(listenfd, (struct sockaddr *)&sun, &len,
+	    SOCK_CLOEXEC | SOCK_NONBLOCK)) == -1) {
+		/*
+		 * Pause accept if we are out of file descriptors, or
+		 * libevent will haunt us here too.
+		 */
+		if (errno == ENFILE || errno == EMFILE) {
+			struct timeval evtpause = { 1, 0 };
+
+			event_del(&control_state.ev);
+			evtimer_add(&control_state.evt, &evtpause);
+		} else if (errno != EWOULDBLOCK && errno != EINTR &&
+		    errno != ECONNABORTED)
+			log_warn("%s: accept4", __func__);
+		return;
+	}
+
+	if ((c = calloc(1, sizeof(struct ctl_conn))) == NULL) {
+		log_warn("%s: calloc", __func__);
+		close(connfd);
+		return;
+	}
+
+	imsg_init(&c->iev.ibuf, connfd);
+	c->iev.handler = control_dispatch_imsg;
+	c->iev.events = EV_READ;
+	event_set(&c->iev.ev, c->iev.ibuf.fd, c->iev.events,
+	    c->iev.handler, &c->iev);
+	event_add(&c->iev.ev, NULL);
+
+	TAILQ_INSERT_TAIL(&ctl_conns, c, entry);
+}
+
+struct ctl_conn *
+control_connbyfd(int fd)
+{
+	struct ctl_conn	*c;
+
+	TAILQ_FOREACH(c, &ctl_conns, entry) {
+		if (c->iev.ibuf.fd == fd)
+			break;
+	}
+
+	return (c);
+}
+
+struct ctl_conn *
+control_connbypid(pid_t pid)
+{
+	struct ctl_conn	*c;
+
+	TAILQ_FOREACH(c, &ctl_conns, entry) {
+		if (c->iev.ibuf.pid == pid)
+			break;
+	}
+
+	return (c);
+}
+
+void
+control_close(int fd)
+{
+	struct ctl_conn	*c;
+
+	if ((c = control_connbyfd(fd)) == NULL) {
+		log_warnx("%s: fd %d: not found", __func__, fd);
+		return;
+	}
+
+	msgbuf_clear(&c->iev.ibuf.w);
+	TAILQ_REMOVE(&ctl_conns, c, entry);
+
+	event_del(&c->iev.ev);
+	close(c->iev.ibuf.fd);
+
+	/* Some file descriptors are available again. */
+	if (evtimer_pending(&control_state.evt, NULL)) {
+		evtimer_del(&control_state.evt);
+		event_add(&control_state.ev, NULL);
+	}
+
+	free(c);
+}
+
+void
+control_dispatch_imsg(int fd, short event, void *bula)
+{
+	struct ctl_conn	*c;
+	struct imsg	 imsg;
+	ssize_t		 n;
+	int		 verbose;
+
+	if ((c = control_connbyfd(fd)) == NULL) {
+		log_warnx("%s: fd %d: not found", __func__, fd);
+		return;
+	}
+
+	if (event & EV_READ) {
+		if (((n = imsg_read(&c->iev.ibuf)) == -1 && errno != EAGAIN) ||
+		    n == 0) {
+			control_close(fd);
+			return;
+		}
+	}
+	if (event & EV_WRITE) {
+		if (msgbuf_write(&c->iev.ibuf.w) <= 0 && errno != EAGAIN) {
+			control_close(fd);
+			return;
+		}
+	}
+
+	for (;;) {
+		if ((n = imsg_get(&c->iev.ibuf, &imsg)) == -1) {
+			control_close(fd);
+			return;
+		}
+		if (n == 0)
+			break;
+
+		switch (imsg.hdr.type) {
+		case IMSG_CTL_RELOAD:
+			listener_imsg_compose_main(imsg.hdr.type, 0, NULL, 0);
+			break;
+		case IMSG_CTL_LOG_VERBOSE:
+			if (IMSG_DATA_SIZE(imsg) != sizeof(verbose))
+				break;
+
+			/* Forward to all other processes. */
+			listener_imsg_compose_main(imsg.hdr.type, imsg.hdr.pid,
+			    imsg.data, IMSG_DATA_SIZE(imsg));
+
+			/* XXX: send to every client? */
+
+			memcpy(&verbose, imsg.data, sizeof(verbose));
+			log_setverbose(verbose);
+			break;
+		default:
+			log_debug("%s: error handling imsg %d", __func__,
+			    imsg.hdr.type);
+			break;
+		}
+		imsg_free(&imsg);
+	}
+
+	imsg_event_add(&c->iev);
+}
+
+int
+control_imsg_relay(struct imsg *imsg)
+{
+	struct ctl_conn	*c;
+
+	if ((c = control_connbypid(imsg->hdr.pid)) == NULL)
+		return (0);
+
+	return (imsg_compose_event(&c->iev, imsg->hdr.type, 0, imsg->hdr.pid,
+	    -1, imsg->data, IMSG_DATA_SIZE(*imsg)));
+}
blob - /dev/null
blob + 81ce3ca4ad9e77106d6d9b9d2f3d4af999039bdc (mode 644)
--- /dev/null
+++ control.h
@@ -0,0 +1,28 @@
+/*
+ * Copyright (c) 2003, 2004 Henning Brauer <henning@openbsd.org>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#ifndef CONTROL_H
+#define CONTROL_H
+
+#include "compat.h"
+
+int	control_init(const char *);
+int	control_listen(int fd);
+void	control_accept(int, short, void *);
+void	control_dispatch_imsg(int, short, void *);
+int	control_imsg_relay(struct imsg *);
+
+#endif
blob - /dev/null
blob + 491222d3473842f9bcedd0560be0a98b6150472b (mode 644)
--- /dev/null
+++ ctl_parser.c
@@ -0,0 +1,151 @@
+/*
+ * Copyright (c) 2004 Esben Norby <norby@openbsd.org>
+ * Copyright (c) 2003, 2004 Henning Brauer <henning@openbsd.org>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <stdio.h>
+#include <string.h>
+
+#include "ctl_parser.h"
+#include "kamid.h"
+
+enum token_type {
+	NOTOKEN,
+	ENDTOKEN,
+	KEYWORD,
+};
+
+
+struct token {
+	enum token_type		 type;
+	const char		*keyword;
+	int			 value;
+	const struct token	*next;
+};
+
+static const struct token t_main[];
+static const struct token t_log[];
+
+static const struct token t_main[] = {
+	{KEYWORD,	"reload",	RELOAD,		NULL},
+	{KEYWORD,	"log",		NONE,		t_log},
+	{ENDTOKEN,	"",		NONE,		NULL},
+};
+
+static const struct token t_log[] = {
+	{KEYWORD,	"verbose",	LOG_VERBOSE,	NULL},
+	{KEYWORD,	"brief",	LOG_BRIEF,	NULL},
+	{ENDTOKEN,	"",		NONE,		NULL},
+};
+
+static const struct token *match_token(const char *, const struct token *,
+    struct parse_result *);
+static void show_valid_args(const struct token *);
+
+struct parse_result *
+parse(int argc, char **argv)
+{
+	static struct parse_result	 res;
+	const struct token		*table = t_main;
+	const struct token		*match;
+
+	memset(&res, 0, sizeof(res));
+
+	while (argc >= 0) {
+		if ((match = match_token(argv[0], table, &res)) == NULL) {
+			fprintf(stderr, "valid commands/args:\n");
+			show_valid_args(table);
+			return NULL;
+		}
+
+		argc--;
+		argv++;
+
+		if (match->type == NOTOKEN || match->next == NULL)
+			break;
+
+		table = match->next;
+	}
+
+	if (argc > 0) {
+		fprintf(stderr, "superfluous argument: %s\n", argv[0]);
+		return NULL;
+	}
+
+	return &res;
+}
+
+static const struct token *
+match_token(const char *word, const struct token *table,
+    struct parse_result *res)
+{
+	size_t			 i, match;
+	const struct token	*t = NULL;
+
+	match = 0;
+
+	for (i = 0; table[i].type != ENDTOKEN; i++) {
+		switch (table[i].type) {
+		case NOTOKEN:
+			if (word == NULL || strlen(word) == 0) {
+				match++;
+				t = &table[i];
+			}
+			break;
+		case KEYWORD:
+			if (word != NULL && strncmp(word, table[i].keyword,
+			    strlen(word)) == 0) {
+				match++;
+				t = &table[i];
+				if (t->value)
+					res->action = t->value;
+			}
+			break;
+		case ENDTOKEN:
+			break;
+		}
+	}
+
+	if (match != 1) {
+		if (word == NULL)
+			fprintf(stderr, "missing argument:\n");
+		else if (match > 1)
+			fprintf(stderr, "ambiuous argument: %s\n", word);
+		else if (match < 1)
+			fprintf(stderr, "unknown argument: %s\n", word);
+		return NULL;
+	}
+
+	return t;
+}
+
+static void
+show_valid_args(const struct token *table)
+{
+	int i;
+
+	for (i = 0; table[i].type != ENDTOKEN; i++) {
+		switch (table[i].type) {
+		case NOTOKEN:
+			fprintf(stderr, "  <cr>\n");
+			break;
+		case KEYWORD:
+			fprintf(stderr, "  %s\n", table[i].keyword);
+			break;
+		case ENDTOKEN:
+			break;
+		}
+	}
+}
blob - /dev/null
blob + 1c41496f316a505b67099be11f5f5d3deec51a7b (mode 644)
--- /dev/null
+++ ctl_parser.h
@@ -0,0 +1,34 @@
+/*
+ * Copyright (c) 2004 Esben Norby <norby@openbsd.org>
+ * Copyright (c) 2003, 2004 Henning Brauer <henning@openbsd.org>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#ifndef CTL_PARSER_H
+#define CTL_PARSER_H
+
+enum actions {
+	NONE,
+	LOG_VERBOSE,
+	LOG_BRIEF,
+	RELOAD,
+};
+
+struct parse_result {
+	enum actions	action;
+};
+
+struct parse_result	*parse(int, char **);
+
+#endif
blob - /dev/null
blob + a15881b0f5c97d15001742bbfee8b7783630fc90 (mode 644)
--- /dev/null
+++ kamictl.8
@@ -0,0 +1,61 @@
+.\" Copyright (c) 2021 Omar Polo <op@omarpolo.com>
+.\"
+.\" Permission to use, copy, modify, and distribute this software for any
+.\" purpose with or without fee is hereby granted, provided that the above
+.\" copyright notice and this permission notice appear in all copies.
+.\"
+.\" THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+.\" WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+.\" MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+.\" ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+.\" WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+.\" ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+.\" OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+.\"
+.Dd $Mdocdate: July 07 2021 $
+.Dt KAMICTL 8
+.Os
+.Sh NAME
+.Nm kamictl
+.Nd control the kamid daemon
+.Sh SYNOPSIS
+.Nm
+.Op Fl s Ar socket
+.Ar command
+.Op Ar argument ...
+.Sh DESCRIPTION
+The
+.Nm
+program contrlos the
+.Xr kamid 8
+daemon.
+.Pp
+The following options are available:
+.Bl -tag -width Ds
+.It Fl s Ar socket
+Use
+.Ar socket
+instead of the default
+.Pa /var/run/kamid.sock
+to communicate with
+.Xr kamid 8 .
+.El
+.Pp
+The following commands are available:
+.Bl -tag -width Ds
+.It Cm log brief
+Disable verbose debug logging.
+.It Cm log verbose
+Enable verbose debug logging.
+.It Cm reload
+Reload the configuration file.
+.El
+.Sh FILES
+.Bl -tag -width "/var/run/kamid.sockXX" -compact
+.It Pa /var/run/kamid.sock
+UNIX-domain socket used for communication with
+.Xr kamid 8 .
+.El
+.Sh SEE ALSO
+.Xr kamid.conf 5 ,
+.Xr kamid 8
blob - /dev/null
blob + e6fc64817d33ec6dfb448540ad0c05c9151903bd (mode 644)
--- /dev/null
+++ kamictl.c
@@ -0,0 +1,140 @@
+/*
+ * Copyright (c) 2021 Omar Polo <op@omarpolo.com>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <sys/socket.h>
+#include <sys/un.h>
+
+#include <err.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <syslog.h>
+#include <unistd.h>
+
+#include "ctl_parser.h"
+#include "kamid.h"
+#include "log.h"
+
+__dead void	 usage(void);
+
+struct imsgbuf	*ibuf;
+
+__dead void
+usage(void)
+{
+	/*
+	 * XXX: this will print `kamid' if compat/getprogname.c is
+	 * used.
+	 */
+	fprintf(stderr, "usage: %s [-s socket] command [argument ...]\n",
+	    getprogname());
+	exit(1);
+}
+
+int
+main(int argc, char **argv)
+{
+	struct sockaddr_un	 sun;
+	struct parse_result	*res;
+	struct imsg		 imsg;
+	int			 ctl_sock;
+	int			 done = 0;
+	int			 n, verbose = 0;
+	int			 ch;
+	const char		*sockname;
+
+	log_init(1, LOG_DAEMON); /* Log to stderr. */
+
+	sockname = KD_SOCKET;
+	while ((ch = getopt(argc, argv, "s:")) != -1) {
+		switch (ch) {
+		case 's':
+			sockname = optarg;
+			break;
+		default:
+			usage();
+		}
+	}
+	argc -= optind;
+	argv += optind;
+
+	/* parse command line */
+	if ((res = parse(argc, argv)) == NULL)
+		exit(1);
+
+	/* connect to control socket */
+	if ((ctl_sock = socket(AF_UNIX, SOCK_STREAM, 0)) == -1)
+		err(1, "socket");
+
+	memset(&sun, 0, sizeof(sun));
+	sun.sun_family = AF_UNIX;
+	strlcpy(sun.sun_path, sockname, sizeof(sun.sun_path));
+
+	if (connect(ctl_sock, (struct sockaddr*)&sun, sizeof(sun)) == -1)
+		err(1, "connect: %s", sockname);
+
+#ifdef __OpenBSD__
+	if (pledge("stdio", NULL) == -1)
+		err(1, "pledge");
+#endif
+
+	if ((ibuf = calloc(1, sizeof(*ibuf))) == NULL)
+		err(1, NULL);
+	imsg_init(ibuf, ctl_sock);
+	done = 0;
+
+	/* process user request */
+	switch (res->action) {
+	case LOG_VERBOSE:
+		verbose = 1;
+		/* fallthrough */
+	case LOG_BRIEF:
+		imsg_compose(ibuf, IMSG_CTL_LOG_VERBOSE, 0, 0, -1,
+		    &verbose, sizeof(verbose));
+		puts("logging request sent.");
+		done = 1;
+		break;
+	case RELOAD:
+		imsg_compose(ibuf, IMSG_CTL_RELOAD, 0, 0, -1, NULL, 0);
+		puts("reload request sent.");
+		done = 1;
+		break;
+	default:
+		usage();
+	}
+
+	imsg_flush(ibuf);
+
+	/*
+	 * Later we may add commands which requires a response.
+	 */
+	while (!done) {
+		if ((n = imsg_get(ibuf, &imsg)) == -1)
+			errx(1, "imsg_get error");
+		if (n == 0)
+			break;
+
+		switch (res->action) {
+		default:
+			break;
+		}
+		imsg_free(&imsg);
+	}
+	close(ctl_sock);
+	free(ibuf);
+
+	return 0;
+}
blob - /dev/null
blob + a9379514f81edfcf3e8b1d67858e6df371adf8f4 (mode 644)
--- /dev/null
+++ kamid.8
@@ -0,0 +1,73 @@
+.\" Copyright (c) 2021 Omar Polo <op@omarpolo.com>
+.\"
+.\" Permission to use, copy, modify, and distribute this software for any
+.\" purpose with or without fee is hereby granted, provided that the above
+.\" copyright notice and this permission notice appear in all copies.
+.\"
+.\" THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+.\" WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+.\" MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+.\" ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+.\" WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+.\" ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+.\" OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+.\"
+.Dd $Mdocdate: July 07 2021 $
+.Dt KAMID 8
+.Os
+.Sh NAME
+.Nm kamid
+.Nd 9p file server daemon
+.Sh SYNOPSYS
+.Nm
+.Op Fl dnv
+.Op Fl D Ar macro Ns = Ns Ar value
+.Op Fl f Pa file
+.Op Fl s Pa socket
+.Sh DESCRIPTION
+.Nm
+is a 9p file server daemon.
+.Pp
+The options are as follows:
+.Bl -tag -width Ds
+.It Fl D Ar macro Ns = Ns Ar value
+Set a
+.Ar macro
+to a
+.Ar value .
+Macros can be referenced in the configuration files.
+.It Fl d
+Do not daemonize.
+If this option is specified,
+.Nm
+will run in the foreground and log to
+.Em stderr .
+.It Fl f Ar file
+specify an alternative configuration file.
+.It Fl n
+Configtest mode.
+Only check the configuration file for validity.
+.It Fl s Ar socket
+Use an alternate location for the default control socket.
+.It Fl v
+Produce more verbose output.
+.El
+.Sh FILES
+.Bl -tag -width "/var/run/kamid.sockXX" -compact
+.It Pa /etc/kamid.conf
+Default
+.Nm
+configuration file.
+.It Pa /var/run/kamid.sock
+UNIX-domain socket used for communication with
+.Xr kamictl 8 .
+.El
+.Sh SEE ALSO
+.Xr kami.conf 5 ,
+.Xr kamictl 8
+.Sh AUTHORS
+.An -nosplit
+The
+.Nm
+program was written by
+.An Omar Polo Aq Mt op@omarpolo.com .
blob - /dev/null
blob + fe394808955b6c3901b3fdf11aaa08dc44c72ff3 (mode 644)
--- /dev/null
+++ kamid.c
@@ -0,0 +1,557 @@
+/*
+ * Copyright (c) 2021 Omar Polo <op@omarpolo.com>
+ * Copyright (c) 2018 Florian Obser <florian@openbsd.org>
+ * Copyright (c) 2005 Claudio Jeker <claudio@openbsd.org>
+ * Copyright (c) 2004 Esben Norby <norby@openbsd.org>
+ * Copyright (c) 2003, 2004 Henning Brauer <henning@openbsd.org>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <sys/socket.h>
+#include <sys/wait.h>
+
+#include <arpa/inet.h>
+#include <netinet/in.h>
+
+#include <errno.h>
+#include <fcntl.h>
+#include <pwd.h>
+#include <signal.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <syslog.h>
+#include <unistd.h>
+
+#include "client.h"
+#include "control.h"
+#include "kamid.h"
+#include "listener.h"
+#include "log.h"
+#include "sandbox.h"
+#include "utils.h"
+
+enum kd_process {
+	PROC_MAIN,
+	PROC_LISTENER,
+	PROC_CLIENTCONN,
+};
+
+const char	*saved_argv0;
+static int	 debug, nflag;
+int		 verbose;
+
+__dead void	usage(void);
+
+void		main_sig_handler(int, short, void *);
+void		main_dispatch_listener(int, short, void *);
+int		main_reload(void);
+int		main_imsg_send_config(struct kd_conf *);
+void		main_dispatch_listener(int, short, void *);
+__dead void	main_shutdown(void);
+
+static pid_t	start_child(enum kd_process, int, int, int);
+
+struct kd_conf		*main_conf;
+static struct imsgev	*iev_listener;
+const char		*conffile;
+pid_t			 listener_pid;
+uint32_t		 cmd_opts;
+
+__dead void
+usage(void)
+{
+	fprintf(stderr, "usage: %s [-dnv] [-f file] [-s socket]\n",
+	    getprogname());
+	exit(1);
+}
+
+int
+main(int argc, char **argv)
+{
+        struct event	 ev_sigint, ev_sigterm, ev_sighup;
+	int		 ch;
+	int		 listener_flag = 0, client_flag = 0;
+	int		 pipe_main2listener[2];
+	int		 control_fd;
+	const char	*csock;
+
+	conffile = KD_CONF_FILE;
+	csock = KD_SOCKET;
+
+	log_init(1, LOG_DAEMON);	/* Log to stderr until deamonized. */
+	log_setverbose(1);
+
+	saved_argv0 = argv[0];
+	if (saved_argv0 == NULL)
+		saved_argv0 = "kamid";
+
+	while ((ch = getopt(argc, argv, "D:df:nsT:v")) != -1) {
+		switch (ch) {
+		case 'D':
+			if (cmdline_symset(optarg) == -1)
+                                log_warnx("could not parse macro definition %s",
+					optarg);
+			break;
+		case 'd':
+			debug = 1;
+			break;
+		case 'f':
+			conffile = optarg;
+			break;
+		case 'n':
+			nflag = 1;
+			break;
+		case 's':
+			csock = optarg;
+			break;
+		case 'T':
+			switch (*optarg) {
+			case 'c':
+				client_flag = 1;
+				break;
+			case 'l':
+				listener_flag = 1;
+				break;
+			default:
+				fatalx("invalid process spec %c", *optarg);
+			}
+			break;
+		case 'v':
+			verbose = 1;
+			break;
+		default:
+			usage();
+		}
+	}
+
+	argc -= optind;
+	argv += optind;
+	if (argc > 0 || (listener_flag && client_flag))
+		usage();
+
+	if (client_flag)
+		client(debug, verbose);
+	else if (listener_flag)
+		listener(debug, verbose);
+
+	if ((main_conf = parse_config(conffile)) == NULL)
+		exit(1);
+
+	if (nflag) {
+		fprintf(stderr, "configuratino OK\n");
+		exit(0);
+	}
+
+	/* Check for root privileges. */
+        if (geteuid())
+                fatalx("need root privileges");
+
+	/* Check for assigned daemon user. */
+	if (getpwnam(KD_USER) == NULL)
+		fatalx("unknown user %s", KD_USER);
+
+	log_init(debug, LOG_DAEMON);
+	log_setverbose(verbose);
+
+	if (!debug)
+		daemon(1, 0);
+
+	log_info("startup");
+
+	if (socketpair(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC | SOCK_NONBLOCK,
+	    PF_UNSPEC, pipe_main2listener) == -1)
+		fatal("main2listener socketpair");
+
+	/* Start children. */
+	listener_pid = start_child(PROC_LISTENER, pipe_main2listener[1],
+	    debug, verbose);
+
+	log_procinit("main");
+
+	event_init();
+
+	/* Setup signal handler */
+	signal_set(&ev_sigint, SIGINT, main_sig_handler, NULL);
+	signal_set(&ev_sigterm, SIGTERM, main_sig_handler, NULL);
+	signal_set(&ev_sighup, SIGHUP, main_sig_handler, NULL);
+
+	signal_add(&ev_sigint, NULL);
+	signal_add(&ev_sigterm, NULL);
+	signal_add(&ev_sighup, NULL);
+
+	signal(SIGCHLD, SIG_IGN);
+	signal(SIGPIPE, SIG_IGN);
+
+	if ((iev_listener = malloc(sizeof(*iev_listener))) == NULL)
+		fatal(NULL);
+	imsg_init(&iev_listener->ibuf, pipe_main2listener[0]);
+	iev_listener->handler = main_dispatch_listener;
+
+	/* Setup event handlers for pipes to listener. */
+	iev_listener->events = EV_READ;
+	event_set(&iev_listener->ev, iev_listener->ibuf.fd,
+	    iev_listener->events, iev_listener->handler, iev_listener);
+	event_add(&iev_listener->ev, NULL);
+
+	if ((control_fd = control_init(csock)) == -1)
+		fatalx("control socket setup failed");
+
+	main_imsg_compose_listener(IMSG_CONTROLFD, control_fd, 0,
+	    NULL, 0);
+	main_imsg_send_config(main_conf);
+
+	sandbox_main();
+
+	event_dispatch();
+
+	main_shutdown();
+	return 0;
+}
+
+void
+main_sig_handler(int sig, short event, void *arg)
+{
+	/*
+	 * Normal signal handler rules don't apply because libevent
+	 * decouples for us.
+	 */
+
+	switch (sig) {
+	case SIGTERM:
+	case SIGINT:
+		main_shutdown();
+		break;
+	case SIGHUP:
+		if (main_reload() == -1)
+			log_warnx("configuration reload failed");
+		else
+			log_debug("configuration reloaded");
+		break;
+	default:
+		fatalx("unexpected signal %d", sig);
+	}
+}
+
+static inline void
+do_auth_tls(struct imsg *imsg)
+{
+	const char *hash, *username = "op";
+	struct passwd *pw;
+	int p[2];
+
+	hash = imsg->data;
+	if (hash[IMSG_DATA_SIZE(*imsg)-1] != '\0')
+		goto err;
+
+	log_debug("tls hash=%s", hash);
+	log_debug("assuming it refers to user `%s'",
+	    username);
+
+	if ((pw = getpwnam(username)) == NULL) {
+		log_warn("getpwnam(%s)", username);
+		goto err;
+	}
+
+	if (socketpair(AF_UNIX, SOCK_STREAM|SOCK_CLOEXEC|SOCK_NONBLOCK,
+	    PF_UNSPEC, p) == -1)
+		fatal("socketpair");
+
+	start_child(PROC_CLIENTCONN, p[1], debug, verbose);
+
+	main_imsg_compose_listener(IMSG_AUTH, p[0], imsg->hdr.peerid,
+	    username, strlen(username)+1);
+	main_imsg_compose_listener(IMSG_AUTH_DIR, -1, imsg->hdr.peerid,
+	    pw->pw_dir, strlen(pw->pw_dir)+1);
+
+	return;
+
+err:
+	main_imsg_compose_listener(IMSG_AUTH, -1, imsg->hdr.peerid,
+	    NULL, 0);
+}
+
+void
+main_dispatch_listener(int fd, short event, void *d)
+{
+	struct imsgev	*iev = d;
+	struct imsgbuf	*ibuf;
+	struct imsg	 imsg;
+	ssize_t		 n;
+	int		 shut = 0;
+
+	ibuf = &iev->ibuf;
+
+	if (event & EV_READ) {
+		if ((n = imsg_read(ibuf)) == -1 && errno != EAGAIN)
+			fatal("imsg_read error");
+		if (n == 0)	/* Connection closed. */
+			shut = 1;
+	}
+	if (event & EV_WRITE) {
+		if ((n = msgbuf_write(&ibuf->w)) == -1 && errno != EAGAIN)
+			fatal("msgbuf_write");
+		if (n == 0)	/* Connection closed. */
+			shut = 1;
+	}
+
+	for (;;) {
+		if ((n = imsg_get(ibuf, &imsg)) == -1)
+			fatal("imsg_get");
+		if (n == 0)	/* No more messages. */
+			break;
+
+		switch (imsg.hdr.type) {
+		case IMSG_AUTH_TLS:
+			do_auth_tls(&imsg);
+			break;
+		default:
+			log_debug("%s: error handling imsg %d", __func__,
+				imsg.hdr.type);
+			break;
+		}
+		imsg_free(&imsg);
+	}
+	if (!shut)
+		imsg_event_add(iev);
+	else {
+		/* This pipe is dead.  Remove its event handler. */
+		event_del(&iev->ev);
+		event_loopexit(NULL);
+	}
+}
+
+int
+main_reload(void)
+{
+	struct kd_conf *xconf;
+
+	if ((xconf = parse_config(conffile)) == NULL)
+		return -1;
+
+	if (main_imsg_send_config(xconf) == -1)
+		return -1;
+
+	merge_config(main_conf, xconf);
+
+	return 0;
+}
+
+static inline int
+make_socket_for(struct kd_listen_conf *l)
+{
+	struct sockaddr_in	addr4;
+	size_t			len;
+	int			fd, v;
+
+	memset(&addr4, 0, sizeof(addr4));
+	addr4.sin_family = AF_INET;
+	addr4.sin_port = htons(l->port);
+	addr4.sin_addr.s_addr = INADDR_ANY;
+
+	if ((fd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
+		fatal("socket");
+
+	v = 1;
+	if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &v, sizeof(v)) == -1)
+		fatal("setsockopt(SO_REUSEADDR)");
+
+	v = 1;
+	if (setsockopt(fd, SOL_SOCKET, SO_REUSEPORT, &v, sizeof(v)) == -1)
+		fatal("setsockopt(SO_REUSEPORT)");
+
+	len = sizeof(addr4);
+	if (bind(fd, (struct sockaddr *)&addr4, len) == -1)
+		fatal("bind(%s, %d)", l->iface, l->port);
+
+	if (listen(fd, 16) == -1)
+		fatal("l(%s, %d)", l->iface, l->port);
+
+	return fd;
+}
+
+int
+main_imsg_send_config(struct kd_conf *xconf)
+{
+	struct kd_pki_conf *pki;
+	struct kd_listen_conf *listen;
+
+#define SEND(type, fd, data, len) do {					\
+		if (main_imsg_compose_listener(type, fd, 0, data, len)	\
+		    == -1)						\
+			return -1;					\
+	} while (0)
+
+	/* Send fixed part of config to children. */
+	SEND(IMSG_RECONF_CONF, -1, xconf, sizeof(*xconf));
+
+	SIMPLEQ_FOREACH(pki, &xconf->pki_head, entry) {
+		log_debug("sending pki %s", pki->name);
+		SEND(IMSG_RECONF_PKI, -1, pki->name, sizeof(pki->name));
+		SEND(IMSG_RECONF_PKI_CERT, -1, pki->cert, pki->certlen);
+		SEND(IMSG_RECONF_PKI_KEY, -1, pki->key, pki->keylen);
+	}
+
+	SIMPLEQ_FOREACH(listen, &xconf->listen_head, entry) {
+		log_debug("sending listen on port %d", listen->port);
+		SEND(IMSG_RECONF_LISTEN, make_socket_for(listen), listen,
+		    sizeof(*listen));
+	}
+
+	SEND(IMSG_RECONF_END, -1, NULL, 0);
+	return 0;
+
+#undef SEND
+}
+
+void
+merge_config(struct kd_conf *conf, struct kd_conf *xconf)
+{
+	/* do stuff... */
+
+	free(xconf);
+}
+
+struct kd_conf *
+config_new_empty(void)
+{
+	struct kd_conf *xconf;
+
+	if ((xconf = calloc(1, sizeof(*xconf))) == NULL)
+		fatal(NULL);
+
+	/* set default values */
+
+	return xconf;
+}
+
+void
+config_clear(struct kd_conf *conf)
+{
+	struct kd_conf *xconf;
+
+	/* Merge current config with an empty one. */
+	xconf = config_new_empty();
+	merge_config(conf, xconf);
+
+	free(conf);
+}
+
+__dead void
+main_shutdown(void)
+{
+	pid_t	pid;
+	int	status;
+
+	/* close pipes. */
+        config_clear(main_conf);
+
+	log_debug("waiting for children to terminate");
+	do {
+		pid = wait(&status);
+		if (pid == -1) {
+			if (errno != EINTR && errno != ECHILD)
+				fatal("wait");
+		} else if (WIFSIGNALED(status))
+			log_warnx("%s terminated; signal %d",
+			    (pid == listener_pid) ? "logger" : "clientconn",
+			    WTERMSIG(status));
+	} while (pid != -1 || (pid == -1 && errno == EINTR));
+
+	free(iev_listener);
+
+	log_info("terminating");
+	exit(0);
+}
+
+static pid_t
+start_child(enum kd_process p, int fd, int debug, int verbose)
+{
+	const char	*argv[5];
+	int		 argc = 0;
+	pid_t		 pid;
+
+	switch (pid = fork()) {
+	case -1:
+		fatal("cannot fork");
+	case 0:
+		break;
+	default:
+		close(fd);
+		return pid;
+	}
+
+	if (fd != 3) {
+		if (dup2(fd, 3) == -1)
+			fatal("cannot setup imsg fd");
+	} else if (fcntl(F_SETFD, 0) == -1)
+		fatal("cannot setup imsg fd");
+
+	argv[argc++] = saved_argv0;
+	switch (p) {
+	case PROC_MAIN:
+		fatalx("Can not start main process");
+	case PROC_LISTENER:
+		argv[argc++] = "-Tl";
+		break;
+	case PROC_CLIENTCONN:
+		argv[argc++] = "-Tc";
+		break;
+	}
+	if (debug)
+		argv[argc++] = "-d";
+	if (verbose)
+		argv[argc++] = "-v";
+	argv[argc++] = NULL;
+
+	/* really? */
+	execvp(saved_argv0, (char *const *)argv);
+	fatal("execvp");
+}
+
+void
+imsg_event_add(struct imsgev *iev)
+{
+	iev->events = EV_READ;
+	if (iev->ibuf.w.queued)
+		iev->events |= EV_WRITE;
+
+	event_del(&iev->ev);
+	event_set(&iev->ev, iev->ibuf.fd, iev->events, iev->handler, iev);
+	event_add(&iev->ev, NULL);
+}
+
+int
+imsg_compose_event(struct imsgev *iev, uint16_t type, uint32_t peerid,
+    pid_t pid, int fd, const void *data, uint16_t datalen)
+{
+	int	ret;
+
+	if ((ret = imsg_compose(&iev->ibuf, type, peerid, pid, fd, data,
+	    datalen) != -1))
+		imsg_event_add(iev);
+
+	return ret;
+}
+
+int
+main_imsg_compose_listener(int type, int fd, uint32_t peerid,
+    const void *data, uint16_t datalen)
+{
+	if (iev_listener)
+		return imsg_compose_event(iev_listener, type, peerid, 0,
+		    fd, data, datalen);
+	else
+		return -1;
+}
blob - /dev/null
blob + 4de54e055192b0c2114fc76dcbbb7ca730e7226e (mode 644)
--- /dev/null
+++ kamid.conf.5
@@ -0,0 +1,27 @@
+.\" Copyright (c) 2021 Omar Polo <op@omarpolo.com>
+.\"
+.\" Permission to use, copy, modify, and distribute this software for any
+.\" purpose with or without fee is hereby granted, provided that the above
+.\" copyright notice and this permission notice appear in all copies.
+.\"
+.\" THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+.\" WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+.\" MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+.\" ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+.\" WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+.\" ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+.\" OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+.\"
+.Dd $Mdocdate: July 07 2021 $
+.Dt KAMID.CONF 5
+.Os
+.Sh NAME
+.Nm kamid.conf
+.Nd 9p file server daemon configuration file
+.Sh DESCRIPTION
+The
+.Xr kamid 8
+daemon is a 9p file server daemon.
+.Sh SEE ALSO
+.Xr kamictl 8 ,
+.Xr kamid 8
blob - /dev/null
blob + ab1b10aa6f2f2b04606cca2b0d1339d419b94e0a (mode 644)
--- /dev/null
+++ kamid.h
@@ -0,0 +1,137 @@
+/*
+ * Copyright (c) 2021 Omar Polo <op@omarpolo.com>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#ifndef KAMID_H
+#define KAMID_H
+
+#include "compat.h"
+
+#include <event.h>
+#include <limits.h>
+#include <stdint.h>
+#include <tls.h>
+
+/* TODO: make these customizable */
+#define KD_CONF_FILE		"/etc/kamid.conf"
+#define KD_USER			"_kamid"
+#define KD_SOCKET		"/var/run/kamid.sock"
+
+#define IMSG_DATA_SIZE(imsg)	((imsg).hdr.len - IMSG_HEADER_SIZE)
+
+struct imsgev {
+	struct imsgbuf	 ibuf;
+	void		(*handler)(int, short, void *);
+	struct event	 ev;
+	short		 events;
+};
+
+enum imsg_type {
+	IMSG_NONE,
+	IMSG_CTL_LOG_VERBOSE,
+	IMSG_CTL_RELOAD,
+	IMSG_CONTROLFD,
+	IMSG_STARTUP,
+	IMSG_RECONF_CONF,
+	IMSG_RECONF_PKI,
+	IMSG_RECONF_PKI_CERT,
+	IMSG_RECONF_PKI_KEY,
+	IMSG_RECONF_LISTEN,
+	IMSG_RECONF_END,
+	IMSG_AUTH,
+	IMSG_AUTH_DIR,
+	IMSG_AUTH_TLS,
+	IMSG_CONN_GONE,
+	IMSG_BUF,
+};
+
+struct kd_options_conf {
+	/* ... */
+};
+
+enum table_type {
+	T_NONE		= 0,
+	T_HASH		= 0x01,
+};
+
+struct table {
+	char			 t_name[LINE_MAX];
+	enum table_type		 t_type;
+	char			 t_path[PATH_MAX];
+	void			*t_handle;
+	struct table_backend	*t_backend;
+};
+
+struct table_backend {
+	const char	*name;
+	int		(*open)(struct table *);
+	int		(*add)(struct table *, const char *, const char *);
+	int		(*lookup)(struct table *, const char *, char **);
+	void		(*close)(struct table *);
+};
+
+/* table_static.c */
+extern struct table_backend table_static;
+
+struct kd_listen_conf {
+	SIMPLEQ_ENTRY(kd_listen_conf)	 entry;
+	uint32_t			 id;
+	int				 fd;
+	char				 iface[LINE_MAX];
+	uint16_t			 port;
+	struct table			*auth_table;
+	char				 pki[LINE_MAX];
+	struct event			 ev;
+	struct tls			*ctx;
+};
+
+struct kd_pki_conf {
+	SIMPLEQ_ENTRY(kd_pki_conf)	 entry;
+	char				 name[LINE_MAX];
+	uint8_t				*cert;
+	size_t				 certlen;
+	uint8_t				*key;
+	size_t				 keylen;
+	struct tls_config		*tlsconf;
+};
+
+struct kd_tables_conf {
+	SIMPLEQ_ENTRY(kd_tables_conf)	 entry;
+	struct table			*table;
+};
+
+struct kd_conf {
+	struct kd_options_conf					 kd_options;
+	SIMPLEQ_HEAD(kd_pki_conf_head, kd_pki_conf)		 pki_head;
+	SIMPLEQ_HEAD(kd_tables_conf_head, kd_tables_conf)	 table_head;
+	SIMPLEQ_HEAD(kd_listen_conf_head, kd_listen_conf)	 listen_head;
+};
+
+/* kamid.c */
+extern int verbose;
+int	main_imsg_compose_listener(int, int, uint32_t, const void *, uint16_t);
+void	merge_config(struct kd_conf *, struct kd_conf *);
+void	imsg_event_add(struct imsgev *);
+int	imsg_compose_event(struct imsgev *, uint16_t, uint32_t, pid_t,
+	    int, const void *, uint16_t);
+
+struct kd_conf	*config_new_empty(void);
+void		 config_clear(struct kd_conf *);
+
+/* parse.y */
+struct kd_conf	*parse_config(const char *);
+int		 cmdline_symset(char *);
+
+#endif
blob - /dev/null
blob + 500d70bd7cd5fa5512a5c46bc2f4fe39b6cc7709 (mode 644)
--- /dev/null
+++ listener.c
@@ -0,0 +1,843 @@
+/*
+ * Copyright (c) 2021 Omar Polo <op@omarpolo.com>
+ * Copyright (c) 2018 Florian Obser <florian@openbsd.org>
+ * Copyright (c) 2004, 2005 Claudio Jeker <claudio@openbsd.org>
+ * Copyright (c) 2004 Esben Norby <norby@openbsd.org>
+ * Copyright (c) 2003, 2004 Henning Brauer <henning@openbsd.org>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include "compat.h"
+
+#include <sys/socket.h>
+#include <sys/tree.h>
+
+#include <errno.h>
+#include <inttypes.h>
+#include <pwd.h>
+#include <signal.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <syslog.h>
+#include <unistd.h>
+
+#include "control.h"
+#include "kamid.h"
+#include "listener.h"
+#include "log.h"
+#include "sandbox.h"
+#include "utils.h"
+
+#define MIN(a, b) ((a) < (b) ? (a) : (b))
+
+static struct kd_conf	*listener_conf;
+static struct imsgev	*iev_main;
+
+static void	listener_sig_handler(int, short, void *);
+__dead void	listener_shutdown(void);
+
+SPLAY_HEAD(clients_tree_id, client) clients;
+
+struct client {
+	uint32_t		 id;
+	int			 fd;
+	int			 done;
+	struct tls		*ctx;
+	struct event		 event;
+	struct imsgev		 iev;
+	struct bufferevent	*bev;
+	SPLAY_ENTRY(client)	 sp_entry;
+};
+
+static void	listener_imsg_event_add(struct imsgev *, void *);
+static void	listener_dispatch_client(int, short, void *);
+static int	listener_imsg_compose_client(struct client *, int,
+    uint32_t, const void *, uint16_t);
+
+static void	apply_config(struct kd_conf *);
+static void	handle_accept(int, short, void *);
+
+static void	handle_handshake(int, short, void *);
+static void	client_read(struct bufferevent *, void *);
+static void	client_write(struct bufferevent *, void *);
+static void	client_error(struct bufferevent *, short, void *);
+static void	client_tls_readcb(int, short, void *);
+static void	client_tls_writecb(int, short, void *);
+static void	close_conn(struct client *);
+static void	handle_close(int, short, void *);
+
+static inline int
+clients_tree_cmp(struct client *a, struct client *b)
+{
+	if (a->id == b->id)
+		return 0;
+	else if (a->id < b->id)
+		return -1;
+	else
+		return +1;
+}
+
+SPLAY_PROTOTYPE(clients_tree_id, client, sp_entry, clients_tree_cmp);
+SPLAY_GENERATE(clients_tree_id, client, sp_entry, clients_tree_cmp)
+
+static void
+listener_sig_handler(int sig, short event, void *d)
+{
+	/*
+	 * Normal signal handler rules don't apply because libevent
+	 * decouples for us.
+	 */
+
+	switch (sig) {
+	case SIGINT:
+	case SIGTERM:
+		listener_shutdown();
+	default:
+		fatalx("unexpected signal %d", sig);
+	}
+}
+
+void
+listener(int debug, int verbose)
+{
+	struct event		 ev_sigint, ev_sigterm;
+	struct passwd		*pw;
+
+	/* listener_conf = config_new_empty(); */
+
+	log_init(debug, LOG_DAEMON);
+	log_setverbose(verbose);
+
+	if ((pw = getpwnam(KD_USER)) == NULL)
+		fatal("getpwnam");
+
+	if (chroot(pw->pw_dir) == -1)
+		fatal("chroot");
+	if (chdir("/") == -1)
+		fatal("chdir(\"/\")");
+
+	setproctitle("listener");
+	log_procinit("listener");
+
+	if (setgroups(1, &pw->pw_gid) ||
+	    setresgid(pw->pw_gid, pw->pw_gid, pw->pw_gid) ||
+	    setresuid(pw->pw_uid, pw->pw_uid, pw->pw_uid))
+		fatal("can't drop privileges");
+
+	event_init();
+
+	/* Setup signal handlers(s). */
+	signal_set(&ev_sigint, SIGINT, listener_sig_handler, NULL);
+	signal_set(&ev_sigterm, SIGTERM, listener_sig_handler, NULL);
+
+	signal_add(&ev_sigint, NULL);
+	signal_add(&ev_sigterm, NULL);
+
+	signal(SIGPIPE, SIG_IGN);
+	signal(SIGHUP, SIG_IGN);
+
+	/* Setup pipe and event handler to the main process. */
+	if ((iev_main = malloc(sizeof(*iev_main))) == NULL)
+		fatal(NULL);
+
+	imsg_init(&iev_main->ibuf, 3);
+	iev_main->handler = listener_dispatch_main;
+
+	/* Setup event handlers. */
+	iev_main->events = EV_READ;
+	event_set(&iev_main->ev, iev_main->ibuf.fd, iev_main->events,
+	    iev_main->handler, iev_main);
+	event_add(&iev_main->ev, NULL);
+
+	sandbox_listener();
+	event_dispatch();
+	listener_shutdown();
+}
+
+__dead void
+listener_shutdown(void)
+{
+	msgbuf_clear(&iev_main->ibuf.w);
+	close(iev_main->ibuf.fd);
+
+	config_clear(listener_conf);
+
+	free(iev_main);
+
+	log_info("listener exiting");
+	exit(0);
+}
+
+static void
+listener_receive_config(struct imsg *imsg, struct kd_conf **nconf,
+    struct kd_pki_conf **pki)
+{
+	struct kd_listen_conf *listen;
+	char *t;
+
+	switch (imsg->hdr.type) {
+	case IMSG_RECONF_CONF:
+		if (*nconf != NULL)
+			fatalx("%s: IMSG_RECONF_CONF already in "
+			    "progress", __func__);
+
+		if (listener_conf != NULL)
+			fatalx("%s: don't know how reload the "
+			    "configuration yet", __func__);
+
+		if (IMSG_DATA_SIZE(*imsg) != sizeof(struct kd_conf))
+			fatalx("%s: IMSG_RECONF_CONF wrong length: %lu",
+			    __func__, IMSG_DATA_SIZE(*imsg));
+		if ((*nconf = malloc(sizeof(**nconf))) == NULL)
+			fatal(NULL);
+		memcpy(*nconf, imsg->data, sizeof(**nconf));
+		memset(&(*nconf)->pki_head, 0, sizeof((*nconf)->pki_head));
+		memset(&(*nconf)->table_head, 0, sizeof((*nconf)->table_head));
+		memset(&(*nconf)->listen_head, 0, sizeof((*nconf)->listen_head));
+		break;
+	case IMSG_RECONF_PKI:
+		if (*nconf == NULL)
+			fatalx("%s: IMSG_RECONF_PKI without "
+			    "IMSG_RECONF_CONF", __func__);
+		*pki = xcalloc(1, sizeof(**pki));
+		t = imsg->data;
+		t[IMSG_DATA_SIZE(*imsg)-1] = '\0';
+		strlcpy((*pki)->name, t, sizeof((*pki)->name));
+		break;
+	case IMSG_RECONF_PKI_CERT:
+		if (*pki == NULL)
+			fatalx("%s: IMSG_RECONF_PKI_CERT without "
+			    "IMSG_RECONF_PKI", __func__);
+		(*pki)->certlen = IMSG_DATA_SIZE(*imsg);
+		(*pki)->cert = xmemdup(imsg->data, (*pki)->certlen);
+		break;
+	case IMSG_RECONF_PKI_KEY:
+		if (*pki == NULL)
+			fatalx("%s: IMSG_RECONF_PKI_KEY without "
+			    "IMSG_RECONF_PKI", __func__);
+		(*pki)->keylen = IMSG_DATA_SIZE(*imsg);
+		(*pki)->key = xmemdup(imsg->data, (*pki)->keylen);
+		SIMPLEQ_INSERT_HEAD(&(*nconf)->pki_head, *pki, entry);
+		pki = NULL;
+		break;
+	case IMSG_RECONF_LISTEN:
+		if (*nconf == NULL)
+			fatalx("%s: IMSG_RECONF_LISTEN without "
+			    "IMSG_RECONF_CONF", __func__);
+		if (IMSG_DATA_SIZE(*imsg) != sizeof(*listen))
+			fatalx("%s: IMSG_RECONF_LISTEN wrong length: %lu",
+			    __func__, IMSG_DATA_SIZE(*imsg));
+		listen = xcalloc(1, sizeof(*listen));
+		memcpy(listen, imsg->data, sizeof(*listen));
+		memset(&listen->entry, 0, sizeof(listen->entry));
+		if ((listen->fd = imsg->fd) == -1)
+			fatalx("%s: IMSG_RECONF_LISTEN no fd",
+			    __func__);
+		listen->auth_table = NULL;
+		memset(&listen->ev, 0, sizeof(listen->ev));
+		SIMPLEQ_INSERT_HEAD(&(*nconf)->listen_head, listen, entry);
+		break;
+	case IMSG_RECONF_END:
+		if (*nconf == NULL)
+			fatalx("%s: IMSG_RECONF_END without "
+			    "IMSG_RECONF_CONF", __func__);
+		/* merge_config(listener_conf, nconf); */
+		apply_config(*nconf);
+		*nconf = NULL;
+		break;
+	}
+}
+
+void
+listener_dispatch_main(int fd, short event, void *d)
+{
+	static struct kd_conf		*nconf;
+	static struct kd_pki_conf	*pki;
+	struct client			*client, find;
+	struct imsg			 imsg;
+	struct imsgev			*iev = d;
+	struct imsgbuf			*ibuf;
+	ssize_t				 n;
+	int				 shut = 0;
+
+	ibuf = &iev->ibuf;
+
+	if (event & EV_READ) {
+		if ((n = imsg_read(ibuf)) == -1 && errno != EAGAIN)
+			fatal("imsg_read error");
+		if (n == 0)	/* Connection closed. */
+			shut = 1;
+	}
+	if (event & EV_WRITE) {
+		if ((n = msgbuf_write(&ibuf->w)) == -1 && errno != EAGAIN)
+			fatal("msgbuf_write");
+		if (n == 0)	/* Connection closed. */
+			shut = 1;
+	}
+
+	for (;;) {
+		if ((n = imsg_get(ibuf, &imsg)) == -1)
+			fatal("%s: imsg_get error", __func__);
+		if (n == 0)	/* No more messages. */
+			break;
+
+		switch (imsg.hdr.type) {
+		case IMSG_CONTROLFD:
+			if ((fd = imsg.fd) == -1)
+				fatalx("%s: expected to receive imsg "
+				    "control fd but didn't receive any",
+				    __func__);
+			/* Listen on control socket. */
+			control_listen(fd);
+			break;
+		case IMSG_RECONF_CONF:
+		case IMSG_RECONF_PKI:
+		case IMSG_RECONF_PKI_CERT:
+		case IMSG_RECONF_PKI_KEY:
+		case IMSG_RECONF_LISTEN:
+		case IMSG_RECONF_END:
+			listener_receive_config(&imsg, &nconf, &pki);
+			break;
+		case IMSG_AUTH:
+			find.id = imsg.hdr.peerid;
+			client = SPLAY_FIND(clients_tree_id, &clients, &find);
+			if (client == NULL) {
+				if (imsg.fd == -1)
+					close(imsg.fd);
+				break;
+			}
+			if (imsg.fd == -1) {
+				close_conn(client);
+				break;
+			}
+			imsg_init(&client->iev.ibuf, imsg.fd);
+			client->iev.events = EV_READ;
+			client->iev.handler = listener_dispatch_client;
+			event_set(&client->iev.ev, client->iev.ibuf.fd,
+			    client->iev.events, client->iev.handler, client);
+			listener_imsg_compose_client(client, IMSG_AUTH,
+			    0, imsg.data, IMSG_DATA_SIZE(imsg));
+			break;
+		case IMSG_AUTH_DIR:
+			find.id = imsg.hdr.peerid;
+			client = SPLAY_FIND(clients_tree_id, &clients, &find);
+			if (client == NULL) {
+				log_info("got AUTH_DIR but client gone");
+				break;
+			}
+			listener_imsg_compose_client(client, IMSG_AUTH_DIR,
+			    0, imsg.data, IMSG_DATA_SIZE(imsg));
+			client->bev = bufferevent_new(client->fd,
+			    client_read, client_write, client_error,
+			    client);
+			if (client->bev == NULL) {
+				log_info("failed to allocate client buffer");
+				close_conn(client);
+				return;
+			}
+			event_set(&client->bev->ev_read, client->fd, EV_READ,
+			    client_tls_readcb, client->bev);
+			event_set(&client->bev->ev_write, client->fd, EV_WRITE,
+			    client_tls_writecb, client->bev);
+
+			/* TODO: adjust watermarks */
+                        bufferevent_setwatermark(client->bev, EV_WRITE, 1, 0);
+			bufferevent_setwatermark(client->bev, EV_READ,  1, 0);
+
+			bufferevent_enable(client->bev, EV_READ|EV_WRITE);
+			break;
+		default:
+			log_debug("%s: unexpected imsg %d", __func__,
+			    imsg.hdr.type);
+			break;
+		}
+		imsg_free(&imsg);
+	}
+
+	if (!shut)
+		listener_imsg_event_add(iev, d);
+	else {
+		/* This pipe is dead.  Remove its event handler. */
+		event_del(&iev->ev);
+		log_warnx("pipe closed, shutting down...");
+		event_loopexit(NULL);
+	}
+}
+
+int
+listener_imsg_compose_main(int type, uint32_t peerid, const void *data,
+    uint16_t datalen)
+{
+	return imsg_compose_event(iev_main, type, peerid, 0, -1, data,
+		datalen);
+}
+
+static void
+listener_imsg_event_add(struct imsgev *iev, void *d)
+{
+	iev->events = EV_READ;
+	if (iev->ibuf.w.queued)
+		iev->events |= EV_WRITE;
+
+	event_del(&iev->ev);
+	event_set(&iev->ev, iev->ibuf.fd, iev->events, iev->handler, d);
+	event_add(&iev->ev, NULL);
+}
+
+static void
+listener_dispatch_client(int fd, short event, void *d)
+{
+        struct client	 find, *client = d;
+	struct imsg	 imsg;
+	struct imsgev	*iev;
+	struct imsgbuf	*ibuf;
+	ssize_t		 n;
+	int		 r, shut = 0;
+
+	iev = &client->iev;
+	ibuf = &iev->ibuf;
+
+	if (event & EV_READ) {
+		if ((n = imsg_read(ibuf)) == -1 && errno != EAGAIN)
+			fatal("imsg_read error");
+		if (n == 0)	/* Connection closed */
+			shut = 1;
+	}
+
+	if (event & EV_WRITE) {
+		if ((n = msgbuf_write(&ibuf->w)) == -1 && errno != EAGAIN)
+			fatal("msgbuf_write");
+		if (n == 0)	/* Connection closed. */
+			shut = 1;
+	}
+
+	for (;;) {
+		if ((n = imsg_get(ibuf, &imsg)) == -1)
+			fatal("%s: imsg_get error", __func__);
+		if (n == 0)	/* No more messages. */
+			break;
+
+		switch (imsg.hdr.type) {
+		case IMSG_BUF:
+			find.id = imsg.hdr.peerid;
+			client = SPLAY_FIND(clients_tree_id, &clients, &find);
+			if (client == NULL) {
+				log_info("got IMSG_BUF but client (%d) gone",
+				    imsg.hdr.peerid);
+				break;
+			}
+			r = bufferevent_write(client->bev, imsg.data,
+			    IMSG_DATA_SIZE(imsg));
+			if (r == -1) {
+				log_warn("%s: bufferevent_write failed",
+				    __func__);
+				close_conn(client);
+				break;
+			}
+			break;
+		default:
+			log_debug("%s: unexpected imsg %d", __func__,
+			    imsg.hdr.type);
+			break;
+		}
+		imsg_free(&imsg);
+	}
+
+	if (!shut)
+		listener_imsg_event_add(iev, d);
+	else {
+		/* This pipe is dead.  Remove its handler */
+		log_debug("client proc vanished");
+                close_conn(client);
+	}
+}
+
+static int
+listener_imsg_compose_client(struct client *client, int type,
+    uint32_t peerid, const void *data, uint16_t len)
+{
+	int ret;
+
+	if ((ret = imsg_compose(&client->iev.ibuf, type, peerid, 0, -1,
+	    data, len)) != -1)
+		listener_imsg_event_add(&client->iev, client);
+
+	return ret;
+}
+
+static inline struct kd_pki_conf *
+pki_by_name(const char *name)
+{
+        struct kd_pki_conf *pki;
+
+	SIMPLEQ_FOREACH(pki, &listener_conf->pki_head, entry) {
+		if (!strcmp(name, pki->name))
+			return pki;
+	}
+
+	return NULL;
+}
+
+static void
+apply_config(struct kd_conf *conf)
+{
+	struct kd_pki_conf *pki;
+	struct kd_listen_conf *listen;
+
+	listener_conf = conf;
+
+	/* prepare the various tls_config */
+	SIMPLEQ_FOREACH(pki, &listener_conf->pki_head, entry) {
+		if ((pki->tlsconf = tls_config_new()) == NULL)
+			fatal("tls_config_new");
+		tls_config_verify_client_optional(pki->tlsconf);
+		tls_config_insecure_noverifycert(pki->tlsconf);
+		if (tls_config_set_keypair_mem(pki->tlsconf,
+		    pki->cert, pki->certlen,
+		    pki->key, pki->keylen) == -1)
+			fatalx("tls_config_set_keypair_mem: %s",
+			    tls_config_error(pki->tlsconf));
+	}
+
+	/* prepare and kickoff the listeners */
+	SIMPLEQ_FOREACH(listen, &listener_conf->listen_head, entry) {
+		if ((listen->ctx = tls_server()) == NULL)
+			fatal("tls_server");
+
+		pki = pki_by_name(listen->pki);
+		if (tls_configure(listen->ctx, pki->tlsconf) == -1)
+			fatalx("tls_configure: %s",
+			    tls_config_error(pki->tlsconf));
+
+		event_set(&listen->ev, listen->fd, EV_READ|EV_PERSIST,
+		    handle_accept, listen);
+		event_add(&listen->ev, NULL);
+	}
+}
+
+static inline void
+yield_r(struct client *c, void (*fn)(int, short, void *))
+{
+	if (event_pending(&c->event, EV_WRITE|EV_READ, NULL))
+		event_del(&c->event);
+	event_set(&c->event, c->fd, EV_READ, fn, c);
+	event_add(&c->event, NULL);
+}
+
+static inline void
+yield_w(struct client *c, void (*fn)(int, short, void *))
+{
+	if (event_pending(&c->event, EV_WRITE|EV_READ, NULL))
+		event_del(&c->event);
+	event_set(&c->event, c->fd, EV_WRITE, fn, c);
+	event_add(&c->event, NULL);
+}
+
+static inline uint32_t
+random_id(void)
+{
+	struct client find, *res;
+
+	for (;;) {
+		find.id = arc4random();
+		res = SPLAY_FIND(clients_tree_id, &clients, &find);
+		if (res == NULL)
+			return find.id;
+	}
+}
+
+static void
+handle_accept(int fd, short ev, void *data)
+{
+	struct kd_listen_conf *listen = data;
+	struct client *c;
+	int s;
+
+	if ((s = accept(fd, NULL, NULL)) == -1) {
+		log_warn("accept");
+		return;
+	}
+
+	c = xcalloc(1, sizeof(*c));
+	c->iev.ibuf.fd = -1;
+
+	if (tls_accept_socket(listen->ctx, &c->ctx, s) == -1) {
+		log_warnx("tls_accept_socket: %s",
+		    tls_error(listen->ctx));
+		free(c);
+		close(s);
+		return;
+	}
+
+	c->fd = s;
+	c->id = random_id();
+
+	SPLAY_INSERT(clients_tree_id, &clients, c);
+
+	/* initialize the event */
+	event_set(&c->event, c->fd, EV_READ, NULL, NULL);
+
+	yield_r(c, handle_handshake);
+}
+
+static void
+handle_handshake(int fd, short ev, void *data)
+{
+	struct client *c = data;
+	ssize_t r;
+	const char *hash;
+
+	switch (r = tls_handshake(c->ctx)) {
+	case TLS_WANT_POLLIN:
+		yield_r(c, handle_handshake);
+		return;
+	case TLS_WANT_POLLOUT:
+		yield_w(c, handle_handshake);
+		return;
+	case -1:
+		log_debug("handhsake failed: %s", tls_error(c->ctx));
+		close_conn(c);
+		return;
+	}
+
+	if ((hash = tls_peer_cert_hash(c->ctx)) == NULL) {
+		log_warnx("client didn't provide certificate");
+		close_conn(c);
+		return;
+	}
+
+	listener_imsg_compose_main(IMSG_AUTH_TLS, c->id,
+	    hash, strlen(hash)+1);
+}
+
+static void
+client_read(struct bufferevent *bev, void *d)
+{
+	struct client	*client = d;
+	struct evbuffer	*src = EVBUFFER_INPUT(bev);
+	char		 buf[BUFSIZ];
+	size_t		 len;
+
+	if (!EVBUFFER_LENGTH(src))
+		return;
+
+	len = bufferevent_read(bev, buf, sizeof(buf));
+	if (len == 0) {
+		(*bev->errorcb)(bev, EVBUFFER_READ, bev->cbarg);
+		return;
+	}
+
+	listener_imsg_compose_client(client, IMSG_BUF, client->id, buf, len);
+}
+
+static void
+client_write(struct bufferevent *bev, void *d)
+{
+	/*
+	 * here we can do some fancy logic like deciding when to call
+	 *
+	 *	(*bev->errorcb)(bev, EVBUFFER_WRITE, bev->cbarg)
+	 *
+	 * to signal the end of the transaction.
+	 */
+
+	return;
+}
+
+static void
+client_error(struct bufferevent *bev, short err, void *d)
+{
+	struct client	*client = d;
+	struct evbuffer	*buf;
+
+        if (err & EVBUFFER_ERROR) {
+		if (errno == EFBIG) {
+			bufferevent_enable(bev, EV_READ);
+			return;
+		}
+		log_debug("buffer event error");
+                close_conn(client);
+		return;
+	}
+
+	if (err & EVBUFFER_EOF) {
+                close_conn(client);
+		return;
+	}
+
+	if (err & (EVBUFFER_READ|EVBUFFER_WRITE)) {
+		bufferevent_disable(bev, EV_READ|EV_WRITE);
+		client->done = 1;
+
+		buf = EVBUFFER_OUTPUT(client->bev);
+		if (EVBUFFER_LENGTH(buf) != 0) {
+			/* finish writing all the data first */
+			bufferevent_enable(client->bev, EV_WRITE);
+			return;
+		}
+
+		close_conn(client);
+		return;
+	}
+
+	log_warnx("unknown event error, closing client connection");
+	close_conn(client);
+}
+
+static void
+client_tls_readcb(int fd, short event, void *d)
+{
+	struct bufferevent	*bufev = d;
+	struct client		*client = bufev->cbarg;
+	char			 buf[BUFSIZ];
+	int			 what = EVBUFFER_READ;
+	int			 howmuch = IBUF_READ_SIZE;
+	ssize_t			 ret;
+	size_t			 len;
+
+	if (event == EV_TIMEOUT) {
+		what |= EVBUFFER_TIMEOUT;
+		goto err;
+	}
+
+	if (bufev->wm_read.high != 0)
+		howmuch = MIN(sizeof(buf), bufev->wm_read.high);
+
+        switch (ret = tls_read(client->ctx, buf, howmuch)) {
+	case TLS_WANT_POLLIN:
+	case TLS_WANT_POLLOUT:
+		goto retry;
+	case -1:
+		what |= EVBUFFER_ERROR;
+		goto err;
+	}
+	len = ret;
+
+	if (len == 0) {
+		what |= EVBUFFER_EOF;
+		goto err;
+	}
+
+	if (evbuffer_add(bufev->input, buf, len) == -1) {
+		what |= EVBUFFER_ERROR;
+		goto err;
+	}
+
+	event_add(&bufev->ev_read, NULL);
+
+	len = EVBUFFER_LENGTH(bufev->input);
+	if (bufev->wm_read.low != 0 && len < bufev->wm_read.low)
+		return;
+	if (bufev->wm_read.high != 0 && len > bufev->wm_read.high) {
+		/*
+		 * here we could implement some read pressure
+		 * mechanism.
+		 */
+	}
+
+	if (bufev->readcb != NULL)
+		(*bufev->readcb)(bufev, bufev->cbarg);
+
+	return;
+
+retry:
+	event_add(&bufev->ev_read, NULL);
+	return;
+
+err:
+	(*bufev->errorcb)(bufev, what, bufev->cbarg);
+}
+
+static void
+client_tls_writecb(int fd, short event, void *d)
+{
+	struct bufferevent	*bufev = d;
+	struct client		*client = bufev->cbarg;
+	ssize_t			 ret;
+	size_t			 len;
+	short			 what = EVBUFFER_WRITE;
+
+	if (event == EV_TIMEOUT) {
+		what |= EVBUFFER_TIMEOUT;
+		goto err;
+	}
+
+	if (EVBUFFER_LENGTH(bufev->output) != 0) {
+		ret = tls_write(client->ctx,
+		    EVBUFFER_DATA(bufev->output),
+		    EVBUFFER_LENGTH(bufev->output));
+		switch (ret) {
+		case TLS_WANT_POLLIN:
+		case TLS_WANT_POLLOUT:
+			goto retry;
+		case -1:
+			what |= EVBUFFER_ERROR;
+			goto err;
+		}
+		len = ret;
+		evbuffer_drain(bufev->output, len);
+	}
+
+	if (EVBUFFER_LENGTH(bufev->output) != 0)
+		event_add(&bufev->ev_write, NULL);
+
+	if (bufev->writecb != NULL &&
+	    EVBUFFER_LENGTH(bufev->output) <= bufev->wm_write.low)
+		(*bufev->writecb)(bufev, bufev->cbarg);
+	return;
+
+retry:
+	event_add(&bufev->ev_write, NULL);
+	return;
+
+err:
+	(*bufev->errorcb)(bufev, what, bufev->cbarg);
+}
+
+static void
+close_conn(struct client *c)
+{
+	log_debug("closing connection");
+
+	if (c->iev.ibuf.fd != -1) {
+		listener_imsg_compose_client(c, IMSG_CONN_GONE, 0, NULL, 0);
+		imsg_flush(&c->iev.ibuf);
+		msgbuf_clear(&c->iev.ibuf.w);
+		event_del(&c->iev.ev);
+		close(c->iev.ibuf.fd);
+	}
+
+	handle_close(c->fd, 0, c);
+}
+
+static void
+handle_close(int fd, short ev, void *d)
+{
+	struct client *c = d;
+
+	switch (tls_close(c->ctx)) {
+	case TLS_WANT_POLLIN:
+		yield_r(c, handle_close);
+		return;
+	case TLS_WANT_POLLOUT:
+		yield_w(c, handle_close);
+		return;
+	}
+
+	event_del(&c->event);
+	tls_free(c->ctx);
+	close(c->fd);
+	free(c);
+}
blob - /dev/null
blob + 4869c415ac1237dc481e958ab418713eb08d313b (mode 644)
--- /dev/null
+++ listener.h
@@ -0,0 +1,26 @@
+/*
+ * Copyright (c) 2021 Omar Polo <op@omarpolo.com>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#ifndef LISTENER_H
+#define LISTENER_H
+
+#include "compat.h"
+
+void		listener(int, int);
+void		listener_dispatch_main(int, short, void *);
+int		listener_imsg_compose_main(int, uint32_t, const void *, uint16_t);
+
+#endif
blob - /dev/null
blob + ea61ad443260427cac94b2098146f1138c477520 (mode 644)
--- /dev/null
+++ log.c
@@ -0,0 +1,197 @@
+/*
+ * Copyright (c) 2003, 2004 Henning Brauer <henning@openbsd.org>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include "compat.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdarg.h>
+#include <string.h>
+#include <syslog.h>
+#include <errno.h>
+#include <time.h>
+
+#include "log.h"
+
+static int		 debug;
+static int		 verbose;
+static const char	*log_procname;
+
+void
+log_init(int n_debug, int facility)
+{
+	debug = n_debug;
+	verbose = n_debug;
+	log_procinit(getprogname());
+
+	if (!debug)
+		openlog(getprogname(), LOG_PID | LOG_NDELAY, facility);
+
+	tzset();
+}
+
+void
+log_procinit(const char *procname)
+{
+	if (procname != NULL)
+		log_procname = procname;
+}
+
+void
+log_setverbose(int v)
+{
+	verbose = v;
+}
+
+int
+log_getverbose(void)
+{
+	return (verbose);
+}
+
+void
+logit(int pri, const char *fmt, ...)
+{
+	va_list	ap;
+
+	va_start(ap, fmt);
+	vlog(pri, fmt, ap);
+	va_end(ap);
+}
+
+void
+vlog(int pri, const char *fmt, va_list ap)
+{
+	char	*nfmt;
+	int	 saved_errno = errno;
+
+	if (debug) {
+		/* best effort in out of mem situations */
+		if (asprintf(&nfmt, "%s\n", fmt) == -1) {
+			vfprintf(stderr, fmt, ap);
+			fprintf(stderr, "\n");
+		} else {
+			vfprintf(stderr, nfmt, ap);
+			free(nfmt);
+		}
+		fflush(stderr);
+	} else
+		vsyslog(pri, fmt, ap);
+
+	errno = saved_errno;
+}
+
+void
+log_warn(const char *emsg, ...)
+{
+	char		*nfmt;
+	va_list		 ap;
+	int		 saved_errno = errno;
+
+	/* best effort to even work in out of memory situations */
+	if (emsg == NULL)
+		logit(LOG_ERR, "%s", strerror(saved_errno));
+	else {
+		va_start(ap, emsg);
+
+		if (asprintf(&nfmt, "%s: %s", emsg,
+		    strerror(saved_errno)) == -1) {
+			/* we tried it... */
+			vlog(LOG_ERR, emsg, ap);
+			logit(LOG_ERR, "%s", strerror(saved_errno));
+		} else {
+			vlog(LOG_ERR, nfmt, ap);
+			free(nfmt);
+		}
+		va_end(ap);
+	}
+
+	errno = saved_errno;
+}
+
+void
+log_warnx(const char *emsg, ...)
+{
+	va_list	 ap;
+
+	va_start(ap, emsg);
+	vlog(LOG_ERR, emsg, ap);
+	va_end(ap);
+}
+
+void
+log_info(const char *emsg, ...)
+{
+	va_list	 ap;
+
+	va_start(ap, emsg);
+	vlog(LOG_INFO, emsg, ap);
+	va_end(ap);
+}
+
+void
+log_debug(const char *emsg, ...)
+{
+	va_list	 ap;
+
+	if (verbose) {
+		va_start(ap, emsg);
+		vlog(LOG_DEBUG, emsg, ap);
+		va_end(ap);
+	}
+}
+
+static void
+vfatalc(int code, const char *emsg, va_list ap)
+{
+	static char	s[BUFSIZ];
+	const char	*sep;
+
+	if (emsg != NULL) {
+		(void)vsnprintf(s, sizeof(s), emsg, ap);
+		sep = ": ";
+	} else {
+		s[0] = '\0';
+		sep = "";
+	}
+	if (code)
+		logit(LOG_CRIT, "fatal in %s: %s%s%s",
+		    log_procname, s, sep, strerror(code));
+	else
+		logit(LOG_CRIT, "fatal in %s%s%s", log_procname, sep, s);
+}
+
+void
+fatal(const char *emsg, ...)
+{
+	va_list	ap;
+
+	va_start(ap, emsg);
+	vfatalc(errno, emsg, ap);
+	va_end(ap);
+	exit(1);
+}
+
+void
+fatalx(const char *emsg, ...)
+{
+	va_list	ap;
+
+	va_start(ap, emsg);
+	vfatalc(0, emsg, ap);
+	va_end(ap);
+	exit(1);
+}
blob - /dev/null
blob + 408dec92e050085f6a48c675e9cb0dde00869cd4 (mode 644)
--- /dev/null
+++ log.h
@@ -0,0 +1,44 @@
+/*
+ * Copyright (c) 2003, 2004 Henning Brauer <henning@openbsd.org>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#ifndef LOG_H
+#define LOG_H
+
+#include <stdarg.h>
+#include <sys/cdefs.h>
+
+void	log_init(int, int);
+void	log_procinit(const char *);
+void	log_setverbose(int);
+int	log_getverbose(void);
+void	log_warn(const char *, ...)
+	    __attribute__((__format__ (printf, 1, 2)));
+void	log_warnx(const char *, ...)
+	    __attribute__((__format__ (printf, 1, 2)));
+void	log_info(const char *, ...)
+	    __attribute__((__format__ (printf, 1, 2)));
+void	log_debug(const char *, ...)
+	    __attribute__((__format__ (printf, 1, 2)));
+void	logit(int, const char *, ...)
+	    __attribute__((__format__ (printf, 2, 3)));
+void	vlog(int, const char *, va_list)
+	    __attribute__((__format__ (printf, 2, 0)));
+__dead void fatal(const char *, ...)
+	    __attribute__((__format__ (printf, 1, 2)));
+__dead void fatalx(const char *, ...)
+	    __attribute__((__format__ (printf, 1, 2)));
+
+#endif /* LOG_H */
blob - /dev/null
blob + 974fdb6e052693f211b33ce37bcf8730bf019e03 (mode 644)
--- /dev/null
+++ parse.y
@@ -0,0 +1,884 @@
+/*
+ * Copyright (c) 2021 Omar Polo <op@omarpolo.com>
+ * Copyright (c) 2018 Florian Obser <florian@openbsd.org>
+ * Copyright (c) 2004, 2005 Esben Norby <norby@openbsd.org>
+ * Copyright (c) 2004 Ryan McBride <mcbride@openbsd.org>
+ * Copyright (c) 2002, 2003, 2004 Henning Brauer <henning@openbsd.org>
+ * Copyright (c) 2001 Markus Friedl.  All rights reserved.
+ * Copyright (c) 2001 Daniel Hartmeier.  All rights reserved.
+ * Copyright (c) 2001 Theo de Raadt.  All rights reserved.
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+%{
+#include "compat.h"
+
+#include <sys/stat.h>
+
+#include <ctype.h>
+#include <err.h>
+#include <errno.h>
+#include <event.h>
+#include <inttypes.h>
+#include <limits.h>
+#include <stdarg.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <syslog.h>
+#include <unistd.h>
+
+#include "log.h"
+#include "kamid.h"
+#include "table.h"
+#include "utils.h"
+
+TAILQ_HEAD(files, file)		 files = TAILQ_HEAD_INITIALIZER(files);
+static struct file {
+	TAILQ_ENTRY(file)	 entry;
+	FILE			*stream;
+	char			*name;
+	size_t	 		 ungetpos;
+	size_t			 ungetsize;
+	u_char			*ungetbuf;
+	int			 eof_reached;
+	int			 lineno;
+	int			 errors;
+} *file, *topfile;
+struct file	*pushfile(const char *, int);
+int		 popfile(void);
+int		 check_file_secrecy(int, const char *);
+int		 yyparse(void);
+int		 yylex(void);
+int		 yyerror(const char *, ...)
+    __attribute__((__format__ (printf, 1, 2)))
+    __attribute__((__nonnull__ (1)));
+int		 kw_cmp(const void *, const void *);
+int		 lookup(char *);
+int		 igetc(void);
+int		 lgetc(int);
+void		 lungetc(int);
+int		 findeol(void);
+
+TAILQ_HEAD(symhead, sym)	 symhead = TAILQ_HEAD_INITIALIZER(symhead);
+struct sym {
+	TAILQ_ENTRY(sym)	 entry;
+	int			 used;
+	int			 persist;
+	char			*nam;
+	char			*val;
+};
+
+int	 symset(const char *, const char *, int);
+char	*symget(const char *);
+
+void	 clear_config(struct kd_conf *xconf);
+
+static void		 add_table(const char *, const char *, const char *);
+static struct table	*findtable(const char *name);
+static void		 add_cert(const char *, const char *);
+static void		 add_key(const char *, const char *);
+static void		 add_listen(const char *, uint32_t, const char *, struct table *);
+
+static uint32_t			 counter;
+static struct table		*table;
+static struct kd_conf		*conf;
+static int			 errors;
+
+typedef struct {
+	union {
+		int64_t		 number;
+		char		*string;
+		struct table	*table;
+	} v;
+	int lineno;
+} YYSTYPE;
+
+%}
+
+%token	AUTH
+%token	CERT
+%token	ERROR
+%token	INCLUDE
+%token	KEY
+%token	LISTEN
+%token	NO
+%token	ON
+%token	PKI PORT
+%token	TABLE TLS
+%token	YES
+
+%token	<v.string>	STRING
+%token	<v.number>	NUMBER
+%type	<v.number>	yesno
+%type	<v.string>	string
+%type	<v.table>	tableref
+
+%%
+
+grammar		: /* empty */
+		| grammar include '\n'
+		| grammar '\n'
+		| grammar table '\n'
+		| grammar pki '\n'
+		| grammar listen '\n'
+		| grammar varset '\n'
+		| grammar error '\n'		{ file->errors++; }
+		;
+
+include		: INCLUDE STRING		{
+			struct file	*nfile;
+
+			if ((nfile = pushfile($2, 0)) == NULL) {
+				yyerror("failed to include file %s", $2);
+				free($2);
+				YYERROR;
+			}
+			free($2);
+
+			file = nfile;
+			lungetc('\n');
+		}
+		;
+
+string		: string STRING	{
+			if (asprintf(&$$, "%s %s", $1, $2) == -1) {
+				free($1);
+				free($2);
+				yyerror("string: asprintf");
+				YYERROR;
+			}
+			free($1);
+			free($2);
+		}
+		| STRING
+		;
+
+yesno		: YES	{ $$ = 1; }
+		| NO	{ $$ = 0; }
+		;
+
+optnl		: '\n' optnl		/* zero or more newlines */
+		| /*empty*/
+		;
+
+nl		: '\n' optnl		/* one or more newlines */
+		;
+
+arrow		: '=' '>' ;
+
+comma		: ',' optnl
+		| nl
+		;
+
+varset		: STRING '=' string		{
+			char *s = $1;
+			if (verbose)
+				printf("%s = \"%s\"\n", $1, $3);
+			while (*s++) {
+				if (isspace((unsigned char)*s)) {
+					yyerror("macro name cannot contain "
+					    "whitespace");
+					free($1);
+					free($3);
+					YYERROR;
+				}
+			}
+			if (symset($1, $3, 0) == -1)
+				fatal("cannot store variable");
+			free($1);
+			free($3);
+		}
+		;
+
+pki		: PKI STRING CERT STRING { add_cert($2, $4); }
+		| PKI STRING KEY STRING  { add_key($2, $4); }
+		;
+
+table_kp	: string arrow string {
+			if (table_add(table, $1, $3) == -1)
+				yyerror("can't add to table %s",
+				    table->t_name);
+			free($1);
+			free($3);
+		}
+		;
+
+table_kps	: table_kp
+		| table_kp comma table_kps
+		;
+
+stringel	: STRING {
+			if (table_add(table, $1, NULL) == -1)
+				yyerror("can't add to table %s",
+				    table->t_name);
+			free($1);
+		}
+		;
+
+string_list	: stringel
+		| stringel comma string_list
+		;
+
+table_vals	: table_kps
+		| string_list
+		;
+
+table		: TABLE STRING STRING {
+			char *p;
+
+			if ((p = strchr($3, ':')) == NULL) {
+				yyerror("invalid table %s", $2);
+				YYERROR;
+			}
+
+			*p = '\0';
+			add_table($2, $3, p+1);
+			free($2);
+			free($3);
+		}
+		| TABLE STRING {
+			add_table($2, "static", NULL);
+		} '{' table_vals '}' {
+			table = NULL;
+		}
+		;
+
+tableref	: '<' STRING '>' {
+			$$ = findtable($2);
+			free($2);
+			if ($$ == NULL)
+				YYERROR;
+		}
+		;
+
+listen		: LISTEN ON STRING PORT NUMBER TLS PKI STRING /* AUTH tableref */ {
+			char *iface = $3;
+			uint32_t port = $5;
+			char *pki = $8;
+			struct table *auth = NULL; /* $10 */
+
+			add_listen(iface, port, pki, auth);
+			free(iface);
+			free(pki);
+		}
+		;
+
+%%
+
+struct keywords {
+	const char	*k_name;
+	int		 k_val;
+};
+
+int
+yyerror(const char *fmt, ...)
+{
+	va_list		 ap;
+	char		*msg;
+
+	file->errors++;
+	va_start(ap, fmt);
+	if (vasprintf(&msg, fmt, ap) == -1)
+		fatalx("yyerror vasprintf");
+	va_end(ap);
+	logit(LOG_CRIT, "%s:%d: %s", file->name, yylval.lineno, msg);
+	free(msg);
+	return 0;
+}
+
+int
+kw_cmp(const void *k, const void *e)
+{
+	return strcmp(k, ((const struct keywords *)e)->k_name);
+}
+
+int
+lookup(char *s)
+{
+	/* This has to be sorted always. */
+	static const struct keywords keywords[] = {
+		{"auth",		AUTH},
+		{"cert",		CERT},
+		{"include",		INCLUDE},
+		{"key",			KEY},
+		{"listen",		LISTEN},
+		{"no",			NO},
+		{"on",			ON},
+		{"pki",			PKI},
+		{"port",		PORT},
+		{"table",		TABLE},
+		{"tls",			TLS},
+		{"yes",			YES},
+	};
+	const struct keywords	*p;
+
+	p = bsearch(s, keywords, sizeof(keywords)/sizeof(keywords[0]),
+	    sizeof(keywords[0]), kw_cmp);
+
+	if (p)
+		return p->k_val;
+	else
+		return STRING;
+}
+
+#define START_EXPAND	1
+#define DONE_EXPAND	2
+
+static int	expanding;
+
+int
+igetc(void)
+{
+	int	c;
+
+	while (1) {
+		if (file->ungetpos > 0)
+			c = file->ungetbuf[--file->ungetpos];
+		else
+			c = getc(file->stream);
+
+		if (c == START_EXPAND)
+			expanding = 1;
+		else if (c == DONE_EXPAND)
+			expanding = 0;
+		else
+			break;
+	}
+	return c;
+}
+
+int
+lgetc(int quotec)
+{
+	int		c, next;
+
+	if (quotec) {
+		if ((c = igetc()) == EOF) {
+			yyerror("reached end of file while parsing "
+			    "quoted string");
+			if (file == topfile || popfile() == EOF)
+				return EOF;
+			return quotec;
+		}
+		return c;
+	}
+
+	while ((c = igetc()) == '\\') {
+		next = igetc();
+		if (next != '\n') {
+			c = next;
+			break;
+		}
+		yylval.lineno = file->lineno;
+		file->lineno++;
+	}
+
+	if (c == EOF) {
+		/*
+		 * Fake EOL when hit EOF for the first time. This gets line
+		 * count right if last line in included file is syntactically
+		 * invalid and has no newline.
+		 */
+		if (file->eof_reached == 0) {
+			file->eof_reached = 1;
+			return '\n';
+		}
+		while (c == EOF) {
+			if (file == topfile || popfile() == EOF)
+				return EOF;
+			c = igetc();
+		}
+	}
+	return c;
+}
+
+void
+lungetc(int c)
+{
+	if (c == EOF)
+		return;
+
+	if (file->ungetpos >= file->ungetsize) {
+		void *p = reallocarray(file->ungetbuf, file->ungetsize, 2);
+		if (p == NULL)
+			err(1, "lungetc");
+		file->ungetbuf = p;
+		file->ungetsize *= 2;
+	}
+	file->ungetbuf[file->ungetpos++] = c;
+}
+
+int
+findeol(void)
+{
+	int	c;
+
+	/* Skip to either EOF or the first real EOL. */
+	while (1) {
+		c = lgetc(0);
+		if (c == '\n') {
+			file->lineno++;
+			break;
+		}
+		if (c == EOF)
+			break;
+	}
+	return ERROR;
+}
+
+int
+yylex(void)
+{
+	unsigned char	 buf[8096];
+	unsigned char	*p, *val;
+	int		 quotec, next, c;
+	int		 token;
+
+top:
+	p = buf;
+	while ((c = lgetc(0)) == ' ' || c == '\t')
+		; /* nothing */
+
+	yylval.lineno = file->lineno;
+	if (c == '#')
+		while ((c = lgetc(0)) != '\n' && c != EOF)
+			; /* nothing */
+	if (c == '$' && !expanding) {
+		while (1) {
+			if ((c = lgetc(0)) == EOF)
+				return 0;
+
+			if (p + 1 >= buf + sizeof(buf) - 1) {
+				yyerror("string too long");
+				return findeol();
+			}
+			if (isalnum(c) || c == '_') {
+				*p++ = c;
+				continue;
+			}
+			*p = '\0';
+			lungetc(c);
+			break;
+		}
+		val = symget(buf);
+		if (val == NULL) {
+			yyerror("macro '%s' not defined", buf);
+			return findeol();
+		}
+		p = val + strlen(val) - 1;
+		lungetc(DONE_EXPAND);
+		while (p >= val) {
+			lungetc(*p);
+			p--;
+		}
+		lungetc(START_EXPAND);
+		goto top;
+	}
+
+	switch (c) {
+	case '\'':
+	case '"':
+		quotec = c;
+		while (1) {
+			if ((c = lgetc(quotec)) == EOF)
+				return 0;
+			if (c == '\n') {
+				file->lineno++;
+				continue;
+			} else if (c == '\\') {
+				if ((next = lgetc(quotec)) == EOF)
+					return (0);
+				if (next == quotec || next == ' ' ||
+				    next == '\t')
+					c = next;
+				else if (next == '\n') {
+					file->lineno++;
+					continue;
+				} else
+					lungetc(next);
+			} else if (c == quotec) {
+				*p = '\0';
+				break;
+			} else if (c == '\0') {
+				yyerror("syntax error");
+				return findeol();
+			}
+			if (p + 1 >= buf + sizeof(buf) - 1) {
+				yyerror("string too long");
+				return findeol();
+			}
+			*p++ = c;
+		}
+		yylval.v.string = strdup(buf);
+		if (yylval.v.string == NULL)
+			err(1, "yylex: strdup");
+		return STRING;
+	}
+
+#define allowed_to_end_number(x) \
+	(isspace(x) || x == ')' || x ==',' || x == '/' || x == '}' || x == '=')
+
+	if (c == '-' || isdigit(c)) {
+		do {
+			*p++ = c;
+			if ((size_t)(p-buf) >= sizeof(buf)) {
+				yyerror("string too long");
+				return findeol();
+			}
+		} while ((c = lgetc(0)) != EOF && isdigit(c));
+		lungetc(c);
+		if (p == buf + 1 && buf[0] == '-')
+			goto nodigits;
+		if (c == EOF || allowed_to_end_number(c)) {
+			const char *errstr = NULL;
+
+			*p = '\0';
+			yylval.v.number = strtonum(buf, LLONG_MIN,
+			    LLONG_MAX, &errstr);
+			if (errstr) {
+				yyerror("\"%s\" invalid number: %s",
+				    buf, errstr);
+				return findeol();
+			}
+			return NUMBER;
+		} else {
+nodigits:
+			while (p > buf + 1)
+				lungetc(*--p);
+			c = *--p;
+			if (c == '-')
+				return c;
+		}
+	}
+
+#define allowed_in_string(x) \
+	(isalnum(x) || (ispunct(x) && x != '(' && x != ')' && \
+	x != '{' && x != '}' && \
+	x != '!' && x != '=' && x != '#' && \
+	x != ','))
+
+	if (isalnum(c) || c == ':' || c == '_') {
+		do {
+			*p++ = c;
+			if ((size_t)(p-buf) >= sizeof(buf)) {
+				yyerror("string too long");
+				return findeol();
+			}
+		} while ((c = lgetc(0)) != EOF && (allowed_in_string(c)));
+		lungetc(c);
+		*p = '\0';
+		if ((token = lookup(buf)) == STRING)
+			if ((yylval.v.string = strdup(buf)) == NULL)
+				err(1, "yylex: strdup");
+		return token;
+	}
+	if (c == '\n') {
+		yylval.lineno = file->lineno;
+		file->lineno++;
+	}
+	if (c == EOF)
+		return 0;
+	return c;
+}
+
+int
+check_file_secrecy(int fd, const char *fname)
+{
+	struct stat	st;
+
+	if (fstat(fd, &st)) {
+		log_warn("cannot stat %s", fname);
+		return -1;
+	}
+	if (st.st_uid != 0 && st.st_uid != getuid()) {
+		log_warnx("%s: owner not root or current user", fname);
+		return -1;
+	}
+	if (st.st_mode & (S_IWGRP | S_IXGRP | S_IRWXO)) {
+		log_warnx("%s: group writable or world read/writable", fname);
+		return -1;
+	}
+	return 0;
+}
+
+struct file *
+pushfile(const char *name, int secret)
+{
+	struct file	*nfile;
+
+	if ((nfile = calloc(1, sizeof(struct file))) == NULL) {
+		log_warn("calloc");
+		return NULL;
+	}
+	if ((nfile->name = strdup(name)) == NULL) {
+		log_warn("strdup");
+		free(nfile);
+		return NULL;
+	}
+	if ((nfile->stream = fopen(nfile->name, "r")) == NULL) {
+		log_warn("%s", nfile->name);
+		free(nfile->name);
+		free(nfile);
+		return NULL;
+	} else if (secret &&
+	    check_file_secrecy(fileno(nfile->stream), nfile->name)) {
+		fclose(nfile->stream);
+		free(nfile->name);
+		free(nfile);
+		return NULL;
+	}
+	nfile->lineno = TAILQ_EMPTY(&files) ? 1 : 0;
+	nfile->ungetsize = 16;
+	nfile->ungetbuf = malloc(nfile->ungetsize);
+	if (nfile->ungetbuf == NULL) {
+		log_warn("malloc");
+		fclose(nfile->stream);
+		free(nfile->name);
+		free(nfile);
+		return NULL;
+	}
+	TAILQ_INSERT_TAIL(&files, nfile, entry);
+	return nfile;
+}
+
+int
+popfile(void)
+{
+	struct file	*prev;
+
+	if ((prev = TAILQ_PREV(file, files, entry)) != NULL)
+		prev->errors += file->errors;
+
+	TAILQ_REMOVE(&files, file, entry);
+	fclose(file->stream);
+	free(file->name);
+	free(file->ungetbuf);
+	free(file);
+	file = prev;
+	return file ? 0 : EOF;
+}
+
+struct kd_conf *
+parse_config(const char *filename)
+{
+	struct sym		*sym, *next;
+
+	counter = 0;
+	conf = config_new_empty();
+
+	file = pushfile(filename, 0);
+	if (file == NULL) {
+		free(conf);
+		return NULL;
+	}
+	topfile = file;
+
+	yyparse();
+	errors = file->errors;
+	popfile();
+
+	/* Free macros and check which have not been used. */
+	TAILQ_FOREACH_SAFE(sym, &symhead, entry, next) {
+		if (verbose && !sym->used)
+			fprintf(stderr, "warning: macro '%s' not used\n",
+			    sym->nam);
+		if (!sym->persist) {
+			free(sym->nam);
+			free(sym->val);
+			TAILQ_REMOVE(&symhead, sym, entry);
+			free(sym);
+		}
+	}
+
+	if (errors) {
+		clear_config(conf);
+		return NULL;
+	}
+
+	return conf;
+}
+
+int
+symset(const char *nam, const char *val, int persist)
+{
+	struct sym	*sym;
+
+	TAILQ_FOREACH(sym, &symhead, entry) {
+		if (strcmp(nam, sym->nam) == 0)
+			break;
+	}
+
+	if (sym != NULL) {
+		if (sym->persist == 1)
+			return 0;
+		else {
+			free(sym->nam);
+			free(sym->val);
+			TAILQ_REMOVE(&symhead, sym, entry);
+			free(sym);
+		}
+	}
+	if ((sym = calloc(1, sizeof(*sym))) == NULL)
+		return -1;
+
+	sym->nam = strdup(nam);
+	if (sym->nam == NULL) {
+		free(sym);
+		return -1;
+	}
+	sym->val = strdup(val);
+	if (sym->val == NULL) {
+		free(sym->nam);
+		free(sym);
+		return -1;
+	}
+	sym->used = 0;
+	sym->persist = persist;
+	TAILQ_INSERT_TAIL(&symhead, sym, entry);
+	return 0;
+}
+
+int
+cmdline_symset(char *s)
+{
+	char	*sym, *val;
+	int	ret;
+
+	if ((val = strrchr(s, '=')) == NULL)
+		return -1;
+	sym = strndup(s, val - s);
+	if (sym == NULL)
+		errx(1, "%s: strndup", __func__);
+	ret = symset(sym, val + 1, 1);
+	free(sym);
+
+	return ret;
+}
+
+char *
+symget(const char *nam)
+{
+	struct sym	*sym;
+
+	TAILQ_FOREACH(sym, &symhead, entry) {
+		if (strcmp(nam, sym->nam) == 0) {
+			sym->used = 1;
+			return sym->val;
+		}
+	}
+	return NULL;
+}
+
+void
+clear_config(struct kd_conf *xconf)
+{
+	/* free stuff? */
+
+	free(xconf);
+}
+
+static void
+add_table(const char *name, const char *type, const char *path)
+{
+	if (table_open(conf, name, type, path) == -1)
+		yyerror("can't initialize table %s", name);
+	table = SIMPLEQ_FIRST(&conf->table_head)->table;
+}
+
+static struct table *
+findtable(const char *name)
+{
+	struct kd_tables_conf *i;
+
+	SIMPLEQ_FOREACH(i, &conf->table_head, entry) {
+		if (!strcmp(i->table->t_name, name))
+			return i->table;
+	}
+
+	yyerror("unknown table %s", name);
+	return NULL;
+}
+
+static void
+add_cert(const char *name, const char *path)
+{
+	struct kd_pki_conf *pki;
+
+	SIMPLEQ_FOREACH(pki, &conf->pki_head, entry) {
+		if (strcmp(name, pki->name) != 0)
+			continue;
+
+		if (pki->cert != NULL) {
+			yyerror("duplicate `pki %s cert'", name);
+			return;
+		}
+
+		goto set;
+	}
+
+	pki = xcalloc(1, sizeof(*pki));
+	strlcpy(pki->name, name, sizeof(pki->name));
+	SIMPLEQ_INSERT_HEAD(&conf->pki_head, pki, entry);
+
+set:
+	if ((pki->cert = tls_load_file(path, &pki->certlen, NULL)) == NULL)
+		fatal(NULL);
+}
+
+static void
+add_key(const char *name, const char *path)
+{
+	struct kd_pki_conf *pki;
+
+	SIMPLEQ_FOREACH(pki, &conf->pki_head, entry) {
+		if (strcmp(name, pki->name) != 0)
+			continue;
+
+		if (pki->key != NULL) {
+			yyerror("duplicate `pki %s key'", name);
+			return;
+		}
+
+		goto set;
+	}
+
+	pki = xcalloc(1, sizeof(*pki));
+	strlcpy(pki->name, name, sizeof(pki->name));
+	SIMPLEQ_INSERT_HEAD(&conf->pki_head, pki, entry);
+
+set:
+	if ((pki->key = tls_load_file(path, &pki->keylen, NULL)) == NULL)
+		fatal(NULL);
+}
+
+static void
+add_listen(const char *iface, uint32_t portno, const char *pki, struct table *auth)
+{
+	struct kd_listen_conf *l;
+
+	if (portno >= UINT16_MAX)
+		fatalx("invalid port number: %"PRIu32, portno);
+
+	l = xcalloc(1, sizeof(*l));
+
+	l->id = counter++;
+	l->fd = -1;
+	strlcpy(l->iface, iface, sizeof(l->iface));
+	l->port = portno;
+	l->auth_table = auth;
+	strlcpy(l->pki, pki, sizeof(l->pki));
+
+	SIMPLEQ_INSERT_HEAD(&conf->listen_head, l, entry);
+}
blob - /dev/null
blob + cf1ac0e9438622950da2943dd9476e842bc8531c (mode 644)
--- /dev/null
+++ sandbox.c
@@ -0,0 +1,64 @@
+/*
+ * Copyright (c) 2021 Omar Polo <op@omarpolo.com>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include "log.h"
+#include "sandbox.h"
+
+#ifdef __OpenBSD__
+
+#include <unistd.h>
+
+void
+sandbox_main(void)
+{
+	return;
+}
+
+void
+sandbox_listener(void)
+{
+	return;
+}
+
+void
+sandbox_client(void)
+{
+	return;
+}
+
+#else
+#warning "No sandbox available for this OS"
+
+void
+sandbox_main(void)
+{
+	log_warnx("No sandbox available for this os");
+	return;
+}
+
+void
+sandbox_listener(void)
+{
+	return;
+}
+
+void
+sandbox_client(void)
+{
+	return;
+}
+
+#endif
blob - /dev/null
blob + c964b983ea10e3af850adc627f269bd1051a2619 (mode 644)
--- /dev/null
+++ sandbox.h
@@ -0,0 +1,24 @@
+/*
+ * Copyright (c) 2021 Omar Polo <op@omarpolo.com>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#ifndef SANDBOX_H
+#define SANDBOX_H
+
+void	sandbox_main(void);
+void	sandbox_listener(void);
+void	sandbox_client(void);
+
+#endif
blob - /dev/null
blob + 99669fa4b4f029b50bd79d2407004fdd377a6041 (mode 644)
--- /dev/null
+++ table.c
@@ -0,0 +1,97 @@
+/*
+ * Copyright (c) 2021 Omar Polo <op@omarpolo.com>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include "compat.h"
+
+#include <stdlib.h>
+#include <string.h>
+
+#include "log.h"
+#include "table.h"
+#include "utils.h"
+
+int
+table_open(struct kd_conf *conf, const char *name, const char *type,
+    const char *path)
+{
+	struct table *t;
+	struct kd_tables_conf *entry;
+	struct table_backend backends[] = {
+		table_static,
+	}, *b;
+	size_t i;
+
+	for (i = 0; i < sizeof(backends)/sizeof(backends[0]); ++i) {
+		b = &backends[i];
+		if (!strcmp(type, b->name))
+			goto found;
+	}
+	log_warn("unknown table type %s", type);
+	return -1;
+
+found:
+	if (b->open == NULL) {
+		log_warn("can't open table %s (type %s)",
+		    name, b->name);
+		return -1;
+	}
+
+	t = xcalloc(1, sizeof(*t));
+	strlcpy(t->t_name, name, sizeof(t->t_name));
+	if (path != NULL)
+		strlcpy(t->t_path, path, sizeof(t->t_path));
+	t->t_backend = b;
+
+	if (t->t_backend->open(t) == -1)
+		fatal("can't open table %s (type %s)",
+		    name, path);
+
+	entry = xcalloc(1, sizeof(*entry));
+	entry->table = t;
+	SIMPLEQ_INSERT_HEAD(&conf->table_head, entry, entry);
+	return 0;
+}
+
+int
+table_add(struct table *t, const char *key, const char *val)
+{
+	if (t->t_backend->add == NULL) {
+		log_warn("can't add to table %s (type %s)",
+		    t->t_name, t->t_backend->name);
+		return -1;
+	}
+
+	return t->t_backend->add(t, key, val);
+}
+
+int
+table_lookup(struct table *t, const char *key, char **ret_val)
+{
+	if (t->t_backend->lookup == NULL) {
+		log_warn("can't lookup table %s (type %s)",
+		    t->t_name, t->t_backend->name);
+		return -1;
+	}
+
+	return t->t_backend->lookup(t, key, ret_val);
+}
+
+void
+table_close(struct table *t)
+{
+	if (t->t_backend->close != NULL)
+		t->t_backend->close(t);
+}
blob - /dev/null
blob + 5380d45279b042b87c616571f4f34c053c0cf217 (mode 644)
--- /dev/null
+++ table.h
@@ -0,0 +1,27 @@
+/*
+ * Copyright (c) 2021 Omar Polo <op@omarpolo.com>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#ifndef TABLE_H
+#define TABLE_H
+
+#include "kamid.h"
+
+int	 table_open(struct kd_conf *, const char *, const char *, const char *);
+int	 table_add(struct table *, const char *, const char *);
+int	 table_lookup(struct table *, const char *, char **);
+void	 table_close(struct table *);
+
+#endif
blob - /dev/null
blob + f110f58e673bafb2faaa1f32105ec7382909fd87 (mode 644)
--- /dev/null
+++ table_static.c
@@ -0,0 +1,127 @@
+/*
+ * Copyright (c) 2021 Omar Polo <op@omarpolo.com>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include "compat.h"
+
+#include <ohash.h>
+#include <stdlib.h>
+
+#include "kamid.h"
+#include "utils.h"
+
+static void	*hash_alloc(size_t, void *);
+static void	*hash_calloc(size_t, size_t, void *);
+static void	 hash_free(void *, void *);
+
+static int	 table_static_open(struct table *);
+static int	 table_static_add(struct table *, const char *, const char *);
+static int	 table_static_lookup(struct table *, const char *, char **);
+static void	 table_static_close(struct table *);
+
+struct table_backend table_static = {
+	"static",
+	table_static_open,
+	table_static_add,
+	table_static_lookup,
+	table_static_close,
+};
+
+struct kp {
+	char	*key;
+	char	*val;
+};
+
+static void *
+hash_alloc(size_t len, void *d)
+{
+	return xmalloc(len);
+}
+
+static void *
+hash_calloc(size_t nmemb, size_t size, void *d)
+{
+	return xcalloc(nmemb, size);
+}
+
+static void
+hash_free(void *ptr, void *d)
+{
+	free(ptr);
+}
+
+static int
+table_static_open(struct table *t)
+{
+	struct ohash_info info = {
+		.key_offset = offsetof(struct kp, key),
+		.calloc = hash_calloc,
+		.free = hash_free,
+		.alloc = hash_alloc,
+	};
+
+	t->t_handle = xmalloc(sizeof(struct ohash));
+	ohash_init(t->t_handle, 5, &info);
+	return 0;
+}
+
+int
+table_static_add(struct table *t, const char *key, const char *val)
+{
+	struct kp	*kp;
+	unsigned int	 slot;
+
+	kp = xcalloc(1, sizeof(*kp));
+	kp->key = xstrdup(key);
+	if (val != NULL)
+		kp->val = xstrdup(val);
+
+	slot = ohash_qlookup(t->t_handle, kp->key);
+	ohash_insert(t->t_handle, slot, kp);
+
+	return 0;
+}
+
+int
+table_static_lookup(struct table *t, const char *key, char **ret_val)
+{
+	struct kp	*kp;
+	unsigned int	 slot;
+
+	slot = ohash_qlookup(t->t_handle, key);
+	if ((kp = ohash_find(t->t_handle, slot)) == NULL)
+		return -1;
+
+	*ret_val = xstrdup(kp->val);
+	return 0;
+}
+
+static void
+table_static_close(struct table *t)
+{
+	struct kp	*kp;
+	unsigned int	 i;
+
+	for (kp = ohash_first(t->t_handle, &i);
+	     kp != NULL;
+	     kp = ohash_next(t->t_handle, &i)) {
+		ohash_remove(t->t_handle, i);
+		free(kp->key);
+		free(kp->val);
+		free(kp);
+	}
+
+	free(t->t_handle);
+}
blob - /dev/null
blob + 17258a36da036b89832db92803ec3545d00647eb (mode 644)
--- /dev/null
+++ utils.c
@@ -0,0 +1,64 @@
+/*
+ * Copyright (c) 2021 Omar Polo <op@omarpolo.com>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include "utils.h"
+
+#include <stdlib.h>
+#include <string.h>
+
+#include "log.h"
+#include "utils.h"
+
+void *
+xmalloc(size_t size)
+{
+	void	*r;
+
+	if ((r = malloc(size)) == NULL)
+		fatal("malloc");
+	return r;
+}
+
+void *
+xcalloc(size_t nmemb, size_t size)
+{
+	void	*r;
+
+	if ((r = calloc(nmemb, size)) == NULL)
+		fatal("calloc");
+	return r;
+}
+
+char *
+xstrdup(const char *s)
+{
+	char	*r;
+
+	if ((r = strdup(s)) == NULL)
+		fatal("strdup");
+	return r;
+}
+
+void *
+xmemdup(const void *d, size_t len)
+{
+	void	*r;
+
+	if ((r = malloc(len)) == NULL)
+		fatal("malloc");
+	memcpy(r, d, len);
+	return r;
+}
blob - /dev/null
blob + f1d33819c799d82d87f77f31b0e3ff59e6f9f254 (mode 644)
--- /dev/null
+++ utils.h
@@ -0,0 +1,27 @@
+/*
+ * Copyright (c) 2021 Omar Polo <op@omarpolo.com>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#ifndef UTILS_H
+#define UTILS_H
+
+#include "compat.h"
+
+void	*xmalloc(size_t);
+void	*xcalloc(size_t, size_t);
+char	*xstrdup(const char *);
+void	*xmemdup(const void *, size_t);
+
+#endif