Index | Thread | Search

From:
Omar Polo <op@omarpolo.com>
Subject:
smtpd: change the table protocol
To:
tech@openbsd.org
Cc:
gilles@poolp.org
Date:
Mon, 29 Apr 2024 15:37:45 +0200

Download raw body.

Thread
This changes the transport layer for the smtpd <-> proc tables
communications.

Using imsg here has proven to be quite painful because smtpd internals
(struct definition etc.) needs to be kept in sync with the table
implementations, and it's also hard to actually write external tables.

Instead, a filter-like protocol for tables completely decouples the
implementations and allows to write tables more easily.

All the OpenSMTPD-extras tables were converted to the new stdio protocol
and I intend to commit this together with a diff for ports to avoid a
fallout.

The table themselves will continue to work as they do now, only the
transport layer will change.

The configuration also won't need any change.

Regarding the diff: there's a bit of churn due to the addition of a
field to fork_proc_backend().  When forking the tables we need to
redirect stdout too, and i'm not sure if it's safe to do so for the
queues and schedulers too.  The new flag ensures that we dup() stdout to
the socketpair only for the tables.

The protocol is documented in the added smtpd-tables.7 manual page.

ok? :)

diff a3f92dc1512276794fa729acd22aa26bc42e44ff refs/heads/stdio
commit - a3f92dc1512276794fa729acd22aa26bc42e44ff
commit + 4385fc0d610d39be5ebd36ab6f9261274a2ff479
blob - b5d88a8bd46e4260c1cc9b501d6bcd220eb76a9b
blob + cd27c1989738d914f2d809ea4d048187578625f3
--- usr.sbin/smtpd/makemap.c
+++ usr.sbin/smtpd/makemap.c
@@ -57,7 +57,8 @@ enum output_type {
  * Stub functions so that makemap compiles using minimum object files.
  */
 int
-fork_proc_backend(const char *backend, const char *conf, const char *procname)
+fork_proc_backend(const char *backend, const char *conf, const char *procname,
+    int do_stdout)
 {
 	return (-1);
 }
blob - a3601e5ddab1401593f9540cb47cc914b8372af1
blob + d0404db2b47129a0e70e00943419a239436ee373
--- usr.sbin/smtpd/queue_proc.c
+++ usr.sbin/smtpd/queue_proc.c
@@ -287,7 +287,7 @@ queue_proc_init(struct passwd *pw, int server, const c
 	uint32_t	version;
 	int		fd;
 
-	fd = fork_proc_backend("queue", conf, "queue-proc");
+	fd = fork_proc_backend("queue", conf, "queue-proc", 0);
 	if (fd == -1)
 		fatalx("queue-proc: exiting");
 
blob - 402d36955d263285e6712d0e2cde91c12782d76b
blob + ac1f4310a0c8e48d20fab2d6e8701a9c63fb1c39
--- usr.sbin/smtpd/scheduler_proc.c
+++ usr.sbin/smtpd/scheduler_proc.c
@@ -100,7 +100,7 @@ scheduler_proc_init(const char *conf)
 	int		fd, r;
 	uint32_t	version;
 
-	fd = fork_proc_backend("scheduler", conf, "scheduler-proc");
+	fd = fork_proc_backend("scheduler", conf, "scheduler-proc", 0);
 	if (fd == -1)
 		fatalx("scheduler-proc: exiting");
 
blob - d914b43f705210b504a60a0c2d4587cec6502e72
blob + 8cbc2b1258a936215588cb56ce838ccf95cb2ea5
--- usr.sbin/smtpd/smtpd/Makefile
+++ usr.sbin/smtpd/smtpd/Makefile
@@ -76,7 +76,9 @@ SRCS+=		scheduler_proc.c
 
 SRCS+=		stat_ramstat.c
 
-MAN=		sendmail.8 smtpd.8 smtpd-filters.7 smtpd.conf.5 table.5
+MAN=		sendmail.8 smtpd.8 smtpd-filters.7 smtpd-tables.7
+MAN+=		smtpd.conf.5 table.5
+
 BINDIR=		/usr/sbin
 
 LDADD+=		-levent -lutil -ltls -lssl -lcrypto -lz
blob - /dev/null
blob + 99e76f5c9c127d3885843a11c6cdf2a0f6962c4c (mode 644)
--- /dev/null
+++ usr.sbin/smtpd/smtpd-tables.7
@@ -0,0 +1,272 @@
+.\"	$OpenBSD$
+.\"
+.\" Copyright (c) 2008 Janne Johansson <jj@openbsd.org>
+.\" Copyright (c) 2009 Jacek Masiulaniec <jacekm@dobremiasto.net>
+.\" Copyright (c) 2012 Gilles Chehade <gilles@poolp.org>
+.\" Copyright (c) 2024 Omar Polo <op@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.
+.\"
+.\"
+.Dd $Mdocdate: April 22 2024 $
+.Dt SMTPD-TABLES 7
+.Os
+.Sh NAME
+.Nm smtpd-tables
+.Nd table API for the smtpd daemon
+.Sh DESCRIPTION
+The
+.Xr smtpd 8
+daemon provides a Simple Mail Transfer Protocol (SMTPD) implementation,
+which allows ordinary machines to become Mail eXchangers (MX).
+Some features that are commonly used by MX,
+such as querying databases for user credentials,
+are outside of the scope of SMTP and too complex to fit in
+.Xr smtpd 8 .
+.Pp
+Because an MX may need to provide these features,
+.Xr smtpd 8
+provides an API to implement
+.Xr table 5
+backends with a simple text-based protocol.
+.Sh DESIGN
+.Nm
+are programs that run as unique standalone processes,
+they do not share
+.Xr smtpd 8
+address space.
+They are executed by
+.Xr smtpd 8
+at startup and expected to run in an infinite loop,
+reading events and queries from standard input and
+writing responses to standard output.
+They are not allowed to terminate.
+.Pp
+Because
+.Nm
+are standalone programs that communicate with
+.Xr smtpd 8 ,
+they may run as different users than
+.Xr smtpd 8
+and may be written in any language.
+.Nm
+must not use blocking I/O,
+they must support answering asynchronously to
+.Xr smtpd 8 .
+.Sh PROTOCOL
+The protocol consist of human-readable lines exchanged between
+.Nm
+and
+.Xr smtpd 8 .
+.Pp
+The protocol begins with a handshake.
+First,
+.Xr smtpd 8
+provides
+.Nm
+with general configuration information in the form of
+key-value lines, terminated by
+.Ql config|ready .
+For example:
+.Bd -literal -offset indent
+config|smtpd-version|7.5.0
+config|protocol|0.1
+config|tablename|devs
+config|ready
+.Ed
+.Pp
+Then,
+.Nm
+register the supported services, terminating with
+.Ql register|ready .
+For example:
+.Bd -literal -offset indent
+register|alias
+register|credentials
+register|ready
+.Ed
+.Pp
+Finally,
+.Xr smtpd 8
+can start querying the table.
+For example:
+.Bd -literal -offset indent
+table|0.1|1713795082.354255|devs|lookup|alias|b72508d|op
+.Ed
+.Pp
+The
+.Dq |
+character is used to separate the fields and may only appear
+verbatim in the last field of the payload, in which case it
+should be considered a regular character and not a separator.
+No other field may contain a
+.Dq | .
+.Pp
+Each request has a common set of fields, followed by some
+other fields that are operation-specific.
+The common format consists of a protocol prefix
+.Sq table ,
+the protocol version, the timestamp and the table name.
+For example:
+.Bd -literal -offset indent
+table|0.1|1713795091.202157|devs
+.Ed
+.Pp
+The protocol is inheritly asynchronous, so multiple request
+may be sent without waiting for the table to reply.
+All the replies have a common prefix, followed by the
+operation-specific response.
+The common format consist of a prefix with the operation name
+in followed by
+.Sq -result ,
+and the unique ID of the request.
+For example:
+.Bd -literal -offset indent
+lookup-result|b72508d
+.Ed
+.Pp
+The list of operations, operation-specific parameters and
+responses are as follows:
+.Bl -tag -width Ds
+.It Cm update Ar id
+Ask the table to reload its configuration.
+The result is either
+.Sq ok
+on success or
+.Sq error
+upon a failure to do so.
+.It Cm check Ar service id query
+Check whether
+.Ar query
+is present in the table.
+The result is
+.Sq found
+if found,
+.Sq not-found
+if not, or
+.Sq error
+upon an error.
+.It Cm lookup Ar service id query
+Look up a value in the table for given the
+.Ar query .
+The result is
+.Sq found
+and the value if found,
+.Sq not-found
+if not found, or
+.Sq error
+upon an error.
+.It Cm fetch Ar service id
+Fetch the next item from the table.
+It is only supported for the
+.Ic source
+and
+.Ic relayhost
+services.
+The result is
+.Sq found
+and the value if found,
+.Sq not-found
+if not found, or
+.Sq error
+upon an error.
+.El
+.Pp
+Each service has a specific format for the result.
+The exact syntax for the values and eventually the keys are
+described in
+.Xr table 5 .
+The services and their result format are as follows:
+.Pp
+.Bl -tag -width mailaddrmap -compact
+.It Ic alias
+One or more aliases separated by a comma.
+.It Ic domain
+A domain name.
+.\" XXX are wildcards allowed?
+.It Ic credentials
+The user name, followed by
+.Sq \&:
+and the encrypted password as per
+.Xr smtpctl 8
+.Cm encrypt
+subcommand.
+.It Ic netaddr
+IPv4 and IPv6 address or netmask.
+.It Ic userinfo
+The user id, followed by
+.Sq \&:
+then the group id, then
+.Sq \&:
+and finally the home directory.
+.It Ic source
+IPv4 and IPv6 address.
+.It Ic mailaddr
+An username, a domain or a full email address.
+.It Ic addrname
+Used to map IP addresses to hostnames.
+.\" .It Ic mailaddrmap
+.\" XXX missing K_RELAYHOST, K_STRING and K_REGEX
+.El
+.Sh EXAMPLES
+Assuming the table is called
+.Dq devs ,
+here's an example of a successful
+.Cm update
+transaction:
+.Bd -literal -offset indent
+table|0.1|1713795097.394049|devs|update|478ff0d2
+update-result|478ff0d2|ok
+.Ed
+.Pp
+A
+.Cm check
+request for the
+.Ic netaddr
+service for the 192.168.0.7 IPv4 address which is
+not in the table:
+.Bd -literal -offset indent
+table|0.1|1713795103.314423|devs|check|netaddr|e5862859|192.168.0.7
+check-result|e5862859|not-found
+.Ed
+.Pp
+A successful
+.Cm lookup
+request for the
+.Ic userinfo
+service for the user
+.Sq op :
+.Bd -literal -offset indent
+table|0.1|1713795110.354921|devs|lookup|userinfo|f993c74|op
+lookup-result|f993c74|found|1000:1000:/home/op
+.Ed
+.Pp
+A series of
+.Cm fetch
+requests for the
+.Cm source
+service:
+.Bd -literal -offset indent
+table|0.1|1713795116.227321|devs|fetch|source|189bd3ee
+lookup-result|189bd3ee|found|192.168.1.7
+table|0.1|1713795120.162438|devs|fetch|source|9e4c56d4
+lookup-result|9e4c56d4|found|10.0.0.8
+table|0.1|1713795122.930928|devs|fetch|source|f2c8b906
+lookup-result|f2c8b906|not-found
+.Ed
+.Sh SEE ALSO
+.Xr smtpd 8
+.Sh HISTORY
+.Nm
+first appeared in
+.Ox 7.6 .
blob - 9f9480053e5c3ed896a3b98210403c4b69adad8b
blob + 4f88f7957f04768f459a64c5c95d89618c664fc3
--- usr.sbin/smtpd/smtpd.c
+++ usr.sbin/smtpd/smtpd.c
@@ -1127,7 +1127,8 @@ load_pki_keys(void)
 }
 
 int
-fork_proc_backend(const char *key, const char *conf, const char *procname)
+fork_proc_backend(const char *key, const char *conf, const char *procname,
+    int do_stdout)
 {
 	pid_t		pid;
 	int		sp[2];
@@ -1165,6 +1166,8 @@ fork_proc_backend(const char *key, const char *conf, c
 	if (pid == 0) {
 		/* child process */
 		dup2(sp[0], STDIN_FILENO);
+		if (do_stdout)
+			dup2(sp[0], STDOUT_FILENO);
 		if (closefrom(STDERR_FILENO + 1) == -1)
 			exit(1);
 
blob - db7a8bacdb2c02d0cdfb7dad87c24dc6ab581972
blob + 6e6ef96f151d342d22d9bc5714f7c1a00da0ec70
--- usr.sbin/smtpd/smtpd.h
+++ usr.sbin/smtpd/smtpd.h
@@ -1620,7 +1620,7 @@ const char *proc_name(enum smtp_proc_type);
 const char *proc_title(enum smtp_proc_type);
 const char *imsg_to_str(int);
 void log_imsg(int, int, struct imsg *);
-int fork_proc_backend(const char *, const char *, const char *);
+int fork_proc_backend(const char *, const char *, const char *, int);
 
 
 /* srs.c */
@@ -1640,6 +1640,8 @@ struct stat_value *stat_timespec(struct timespec *);
 
 
 /* table.c */
+const char *table_service_name(enum table_service);
+int table_service_from_name(const char *);
 struct table *table_find(struct smtpd *, const char *);
 struct table *table_create(struct smtpd *, const char *, const char *,
     const char *);
blob - 99db7b0a305e2c9efd8586eeb6535e894b5ae09e
blob + 210d09ca6922c09d768fc101e98a1200ebf2ff5e
--- usr.sbin/smtpd/table.c
+++ usr.sbin/smtpd/table.c
@@ -37,7 +37,6 @@ extern struct table_backend table_backend_db;
 extern struct table_backend table_backend_getpwnam;
 extern struct table_backend table_backend_proc;
 
-static const char * table_service_name(enum table_service);
 static int table_parse_lookup(enum table_service, const char *, const char *,
     union lookup *);
 static int parse_sockaddr(struct sockaddr *, int, const char *);
@@ -67,27 +66,59 @@ table_backend_lookup(const char *backend)
 	return NULL;
 }
 
-static const char *
+const char *
 table_service_name(enum table_service s)
 {
 	switch (s) {
-	case K_NONE:		return "NONE";
-	case K_ALIAS:		return "ALIAS";
-	case K_DOMAIN:		return "DOMAIN";
-	case K_CREDENTIALS:	return "CREDENTIALS";
-	case K_NETADDR:		return "NETADDR";
-	case K_USERINFO:	return "USERINFO";
-	case K_SOURCE:		return "SOURCE";
-	case K_MAILADDR:	return "MAILADDR";
-	case K_ADDRNAME:	return "ADDRNAME";
-	case K_MAILADDRMAP:	return "MAILADDRMAP";
-	case K_RELAYHOST:	return "RELAYHOST";
-	case K_STRING:		return "STRING";
-	case K_REGEX:		return "REGEX";
+	case K_NONE:		return "none";
+	case K_ALIAS:		return "alias";
+	case K_DOMAIN:		return "domain";
+	case K_CREDENTIALS:	return "credentials";
+	case K_NETADDR:		return "netaddr";
+	case K_USERINFO:	return "userinfo";
+	case K_SOURCE:		return "source";
+	case K_MAILADDR:	return "mailaddr";
+	case K_ADDRNAME:	return "addrname";
+	case K_MAILADDRMAP:	return "mailaddrmap";
+	case K_RELAYHOST:	return "relayhost";
+	case K_STRING:		return "string";
+	case K_REGEX:		return "regex";
 	}
 	return "???";
 }
 
+int
+table_service_from_name(const char *service)
+{
+	if (!strcmp(service, "none"))
+		return K_NONE;
+	if (!strcmp(service, "alias"))
+		return K_ALIAS;
+	if (!strcmp(service, "domain"))
+		return K_DOMAIN;
+	if (!strcmp(service, "credentials"))
+		return K_CREDENTIALS;
+	if (!strcmp(service, "netaddr"))
+		return K_NETADDR;
+	if (!strcmp(service, "userinfo"))
+		return K_USERINFO;
+	if (!strcmp(service, "source"))
+		return K_SOURCE;
+	if (!strcmp(service, "mailaddr"))
+		return K_MAILADDR;
+	if (!strcmp(service, "addrname"))
+		return K_ADDRNAME;
+	if (!strcmp(service, "mailaddrmap"))
+		return K_MAILADDRMAP;
+	if (!strcmp(service, "relayhost"))
+		return K_RELAYHOST;
+	if (!strcmp(service, "string"))
+		return K_STRING;
+	if (!strcmp(service, "regex"))
+		return K_REGEX;
+	return (-1);
+}
+
 struct table *
 table_find(struct smtpd *conf, const char *name)
 {
blob - 56893a0fb612c5a65fe5c093e6e392e066b003a4
blob + 700fdf52dd88c5b8c28c43493b56315036099ffb
--- usr.sbin/smtpd/table_proc.c
+++ usr.sbin/smtpd/table_proc.c
@@ -1,6 +1,7 @@
 /*	$OpenBSD: table_proc.c,v 1.17 2021/06/14 17:58:16 eric Exp $	*/
 
 /*
+ * Copyright (c) 2024 Omar Polo <op@openbsd.org>
  * Copyright (c) 2013 Eric Faurot <eric@openbsd.org>
  *
  * Permission to use, copy, modify, and distribute this software for any
@@ -17,87 +18,104 @@
  */
 
 #include <errno.h>
+#include <stdio.h>
+#include <stdlib.h>
 #include <string.h>
+#include <unistd.h>
 
 #include "smtpd.h"
 #include "log.h"
 
+#define PROTOCOL_VERSION	"0.1"
+
 struct table_proc_priv {
-	pid_t		pid;
-	struct imsgbuf	ibuf;
+	FILE		*in;
+	FILE		*out;
+	char		*line;
+	size_t		 linesize;
+
+	/*
+	 * The last ID used in a request.  At the moment the protocol
+	 * is synchronous from our point of view, so it's used to
+	 * assert that the table replied with the correct ID.
+	 */
+	char		 lastid[16];
 };
 
-static struct imsg	 imsg;
-static size_t		 rlen;
-static char		*rdata;
+static char *
+table_proc_nextid(struct table *table)
+{
+	struct table_proc_priv	*priv = table->t_handle;
+	int			 r;
 
-extern char	**environ;
+	r = snprintf(priv->lastid, sizeof(priv->lastid), "%lld",
+	    (unsigned long long)arc4random());
+	if (r < 0 || (size_t)r >= sizeof(priv->lastid))
+		fatal("table-proc: snprintf");
 
+	return (priv->lastid);
+}
+
 static void
-table_proc_call(struct table_proc_priv *p)
+table_proc_send(struct table *table, const char *type, int service,
+    const char *param)
 {
-	ssize_t	n;
+	struct table_proc_priv	*priv = table->t_handle;
+	struct timeval		 tv;
 
-	if (imsg_flush(&p->ibuf) == -1) {
-		log_warn("warn: table-proc: imsg_flush");
-		fatalx("table-proc: exiting");
-	}
+	gettimeofday(&tv, NULL);
+	fprintf(priv->out, "table|%s|%lld.%06ld|%s|%s",
+	    PROTOCOL_VERSION, (long long)tv.tv_sec, (long)tv.tv_usec,
+	    table->t_name, type);
+	if (service != -1) {
+		fprintf(priv->out, "|%s|%s", table_service_name(service),
+		    table_proc_nextid(table));
+		if (param)
+			fprintf(priv->out, "|%s", param);
+		fputc('\n', priv->out);
+	} else
+		fprintf(priv->out, "|%s\n", table_proc_nextid(table));
 
-	while (1) {
-		if ((n = imsg_get(&p->ibuf, &imsg)) == -1) {
-			log_warn("warn: table-proc: imsg_get");
-			break;
-		}
-		if (n) {
-			rlen = imsg.hdr.len - IMSG_HEADER_SIZE;
-			rdata = imsg.data;
+	if (fflush(priv->out) == EOF)
+		fatal("table-proc: fflush");
+}
 
-			if (imsg.hdr.type != PROC_TABLE_OK) {
-				log_warnx("warn: table-proc: bad response");
-				break;
-			}
-			return;
-		}
+static const char *
+table_proc_recv(struct table *table, const char *type)
+{
+	struct table_proc_priv	*priv = table->t_handle;
+	const char		*l;
+	ssize_t			 linelen;
+	size_t			 len;
 
-		if ((n = imsg_read(&p->ibuf)) == -1 && errno != EAGAIN) {
-			log_warn("warn: table-proc: imsg_read");
-			break;
-		}
+	if ((linelen = getline(&priv->line, &priv->linesize, priv->in)) == -1)
+		fatal("table-proc: getline");
+	priv->line[strcspn(priv->line, "\n")] = '\0';
+	l = priv->line;
 
-		if (n == 0) {
-			log_warnx("warn: table-proc: pipe closed");
-			break;
-		}
-	}
+	len = strlen(type);
+	if (strncmp(l, type, len) != 0)
+		goto err;
+	l += len;
 
+	if (*l != '|')
+		goto err;
+	l++;
+
+	len = strlen(priv->lastid);
+	if (strncmp(l, priv->lastid, len) != 0)
+		goto err;
+	l += len;
+
+	if (*l != '|')
+		goto err;
+	return (++l);
+
+ err:
+	log_warnx("warn: table-proc: failed to parse reply");
 	fatalx("table-proc: exiting");
 }
 
-static void
-table_proc_read(void *dst, size_t len)
-{
-	if (len > rlen) {
-		log_warnx("warn: table-proc: bad msg len");
-		fatalx("table-proc: exiting");
-	}
-
-	if (dst)
-		memmove(dst, rdata, len);
-
-	rlen -= len;
-	rdata += len;
-}
-
-static void
-table_proc_end(void)
-{
-	if (rlen) {
-		log_warnx("warn: table-proc: bogus data");
-		fatalx("table-proc: exiting");
-	}
-	imsg_free(&imsg);
-}
-
 /*
  * API
  */
@@ -106,25 +124,54 @@ static int
 table_proc_open(struct table *table)
 {
 	struct table_proc_priv	*priv;
-	struct table_open_params op;
-	int			 fd;
+	const char		*s;
+	ssize_t			 len;
+	int			 service, services = 0;
+	int			 fd, fdd;
 
 	priv = xcalloc(1, sizeof(*priv));
 
-	fd = fork_proc_backend("table", table->t_config, table->t_name);
+	fd = fork_proc_backend("table", table->t_config, table->t_name, 1);
 	if (fd == -1)
 		fatalx("table-proc: exiting");
+	if ((fdd = dup(fd)) == -1) {
+		log_warnx("warn: table-proc: dup");
+		fatalx("table-proc: exiting");
+	}
+	if ((priv->in = fdopen(fd, "r")) == NULL)
+		fatalx("table-proc: fdopen");
+	if ((priv->out = fdopen(fd, "w")) == NULL)
+		fatalx("table-proc: fdopen");
 
-	imsg_init(&priv->ibuf, fd);
+	fprintf(priv->out, "config|smtpd-version|"SMTPD_VERSION"\n");
+	fprintf(priv->out, "config|protocol|"PROTOCOL_VERSION"\n");
+	fprintf(priv->out, "config|tablename|%s\n", table->t_name);
+	fprintf(priv->out, "config|ready\n");
+	if (fflush(priv->out) == EOF)
+		fatalx("table-proc: fflush");
 
-	memset(&op, 0, sizeof op);
-	op.version = PROC_TABLE_API_VERSION;
-	(void)strlcpy(op.name, table->t_name, sizeof op.name);
-	imsg_compose(&priv->ibuf, PROC_TABLE_OPEN, 0, 0, -1, &op, sizeof op);
+	while ((len = getline(&priv->line, &priv->linesize, priv->in)) != -1) {
+		priv->line[strcspn(priv->line, "\n")] = '\0';
 
-	table_proc_call(priv);
-	table_proc_end();
+		if (strncmp(priv->line, "register|", 9) != 0)
+			fatalx("table-proc: invalid handshake reply");
 
+		s = priv->line + 9;
+		if (!strcmp(s, "ready"))
+			break;
+		service = table_service_from_name(s);
+		if (service == -1 || service == K_NONE)
+			fatalx("table-proc: unknown service %s", s);
+
+		services |= service;
+	}
+
+	if (ferror(priv->in))
+		fatalx("table-proc: getline");
+
+	if (services == 0)
+		fatalx("table-proc: no services registered");
+
 	table->t_handle = priv;
 
 	return (1);
@@ -133,16 +180,17 @@ table_proc_open(struct table *table)
 static int
 table_proc_update(struct table *table)
 {
-	struct table_proc_priv	*priv = table->t_handle;
-	int r;
+	const char		*r;
 
-	imsg_compose(&priv->ibuf, PROC_TABLE_UPDATE, 0, 0, -1, NULL, 0);
+	table_proc_send(table, "update", -1, NULL);
+	r = table_proc_recv(table, "update-result");
+	if (!strcmp(r, "ok"))
+		return (1);
+	if (!strcmp(r, "error"))
+		return (0);
 
-	table_proc_call(priv);
-	table_proc_read(&r, sizeof(r));
-	table_proc_end();
-
-	return (r);
+	log_warnx("warn: table-proc: failed parse reply");
+	fatalx("table-proc: exiting");
 }
 
 static void
@@ -150,105 +198,85 @@ table_proc_close(struct table *table)
 {
 	struct table_proc_priv	*priv = table->t_handle;
 
-	imsg_compose(&priv->ibuf, PROC_TABLE_CLOSE, 0, 0, -1, NULL, 0);
-	if (imsg_flush(&priv->ibuf) == -1)
-		fatal("imsg_flush");
+	if (fclose(priv->in) == EOF)
+		fatal("table-proc: fclose");
+	if (fclose(priv->out) == EOF)
+		fatal("table-proc: fclose");
+	free(priv->line);
+	free(priv);
 
 	table->t_handle = NULL;
 }
 
 static int
-imsg_add_params(struct ibuf *buf)
-{
-	size_t count = 0;
-
-	if (imsg_add(buf, &count, sizeof(count)) == -1)
-		return (-1);
-
-	return (0);
-}
-
-static int
 table_proc_lookup(struct table *table, enum table_service s, const char *k, char **dst)
 {
-	struct table_proc_priv	*priv = table->t_handle;
-	struct ibuf		*buf;
-	int			 r;
+	const char		*req = "lookup", *res = "lookup-result";
+	const char		*r;
 
-	buf = imsg_create(&priv->ibuf,
-	    dst ? PROC_TABLE_LOOKUP : PROC_TABLE_CHECK, 0, 0,
-	    sizeof(s) + strlen(k) + 1);
+	if (dst == NULL) {
+		req = "check";
+		res = "check-result";
+	}
 
-	if (buf == NULL)
+	table_proc_send(table, req, s, k);
+	r = table_proc_recv(table, res);
+
+	/* common replies */
+	if (!strcmp(r, "not-found"))
+		return (0);
+	if (!strcmp(r, "error"))
 		return (-1);
-	if (imsg_add(buf, &s, sizeof(s)) == -1)
-		return (-1);
-	if (imsg_add_params(buf) == -1)
-		return (-1);
-	if (imsg_add(buf, k, strlen(k) + 1) == -1)
-		return (-1);
-	imsg_close(&priv->ibuf, buf);
 
-	table_proc_call(priv);
-	table_proc_read(&r, sizeof(r));
-
-	if (r == 1 && dst) {
-		if (rlen == 0) {
-			log_warnx("warn: table-proc: empty response");
-			fatalx("table-proc: exiting");
-		}
-		if (rdata[rlen - 1] != '\0') {
-			log_warnx("warn: table-proc: not NUL-terminated");
-			fatalx("table-proc: exiting");
-		}
-		*dst = strdup(rdata);
-		if (*dst == NULL)
-			r = -1;
-		table_proc_read(NULL, rlen);
+	if (dst == NULL) {
+		/* check op */
+		if (!strncmp(r, "found", 5))
+			return (1);
+		log_warnx("warn: table-proc: failed to parse reply");
+		fatalx("table-proc: exiting");
 	}
 
-	table_proc_end();
-
-	return (r);
+	/* lookup op */
+	if (strncmp(r, "found|", 6) != 0) {
+		log_warnx("warn: table-proc: failed to parse reply");
+		fatalx("table-proc: exiting");
+	}
+	r += 6;
+	if (*r == '\0') {
+		log_warnx("warn: table-proc: empty response");
+		fatalx("table-proc: exiting");
+	}
+	if ((*dst = strdup(r)) == NULL)
+		return (-1);
+	return (1);
 }
 
 static int
 table_proc_fetch(struct table *table, enum table_service s, char **dst)
 {
-	struct table_proc_priv	*priv = table->t_handle;
-	struct ibuf		*buf;
-	int			 r;
+	const char		*r;
 
-	buf = imsg_create(&priv->ibuf, PROC_TABLE_FETCH, 0, 0, sizeof(s));
-	if (buf == NULL)
+	table_proc_send(table, "fetch", s, NULL);
+	r = table_proc_recv(table, "fetch-result");
+
+	if (!strcmp(r, "not-found"))
+		return (0);
+	if (!strcmp(r, "error"))
 		return (-1);
-	if (imsg_add(buf, &s, sizeof(s)) == -1)
-		return (-1);
-	if (imsg_add_params(buf) == -1)
-		return (-1);
-	imsg_close(&priv->ibuf, buf);
 
-	table_proc_call(priv);
-	table_proc_read(&r, sizeof(r));
-
-	if (r == 1) {
-		if (rlen == 0) {
-			log_warnx("warn: table-proc: empty response");
-			fatalx("table-proc: exiting");
-		}
-		if (rdata[rlen - 1] != '\0') {
-			log_warnx("warn: table-proc: not NUL-terminated");
-			fatalx("table-proc: exiting");
-		}
-		*dst = strdup(rdata);
-		if (*dst == NULL)
-			r = -1;
-		table_proc_read(NULL, rlen);
+	if (strncmp(r, "found|", 6) != 0) {
+		log_warnx("warn: table-proc: failed to parse reply");
+		fatalx("table-proc: exiting");
 	}
+	r += 6;
+	if (*r == '\0') {
+		log_warnx("warn: table-proc: empty response");
+		fatalx("table-proc: exiting");
+	}
 
-	table_proc_end();
-
-	return (r);
+	if ((*dst = strdup(r)) == NULL)
+		return (-1);
+	return (1);
 }
 
 struct table_backend table_backend_proc = {