diff -r 8127893f53b1 sys/src/cmd/ndb/dns.h
--- a/sys/src/cmd/ndb/dns.h	Wed Aug 26 21:17:17 2020 -0700
+++ b/sys/src/cmd/ndb/dns.h	Fri Aug 28 20:55:56 2020 -0700
@@ -143,6 +143,9 @@
 
 	Maxpath=	128,	/* size of mntpt */
 	Maxlcks=	10,	/* max. query-type locks per domain name */
+	Maxauth=	16,	/* proto for auth */
+	Maxmdns=	255,	/* Limits for mDNS messages */
+	Maxdial= 	271,	/* port + trns + !! + host */
 
 	RRmagic=	0xdeadbabe,
 	DNmagic=	0xa110a110,
@@ -168,6 +171,7 @@
 typedef struct Request	Request;
 typedef struct SOA	SOA;
 typedef struct Server	Server;
+typedef struct Service	Service;
 typedef struct Sig	Sig;
 typedef struct Srv	Srv;
 typedef struct Txt	Txt;
@@ -247,6 +251,21 @@
 };
 
 /*
+ * registry service
+ */
+struct Service
+{
+	Service *next;
+	char	labl[Maxmdns]; /* Use mDNS limits */
+	char	host[Maxmdns];
+	char	mtpt[Maxpath];
+	char	auth[Maxauth];
+	char	trns[16]; /* transport tcp/ssh/gopher/carrierpidgeon */
+	char	port[8]; /* 9fs, numerical, etc - switch to #? */
+	uchar	perm; /* flag: from db */
+};
+
+/*
  *  an unpacked resource record
  */
 struct RR
@@ -426,10 +445,13 @@
 extern char	*trace;
 extern int	traceactivity;
 extern char	*zonerefreshprogram;
+extern Service  *registry;
 
 #pragma	varargck	type	"R"	RR*
 #pragma	varargck	type	"Q"	RR*
-
+#pragma varargck 	type 	"G" 	Service*
+#pragma varargck	type	"D"	Service*
+#pragma varargck	type	"N"	Service*
 
 /* dn.c */
 extern char	*rrtname[];
@@ -463,7 +485,12 @@
 int	getactivity(Request*, int);
 Area*	inmyarea(char*);
 void	putactivity(int);
+void	reg2cache(void);
+char*	rstr2cache(char*, int);
+char*	rstrdtch(char*);
+char*	rstrupdt(char*);
 RR*	randomize(RR*);
+void	reglog(char*, ...);
 RR*	rralloc(int);
 void	rrattach(RR*, int);
 int	rravfmt(Fmt*);
@@ -523,6 +550,9 @@
 void	dnnotify(DNSmsg*, DNSmsg*, Request*);
 void	notifyproc(void);
 
+/* reglookup.c */
+int	openregistry(void);	
+
 /* convDNS2M.c */
 int	convDNS2M(DNSmsg*, uchar*, int);
 
diff -r 8127893f53b1 sys/src/cmd/ndb/mkfile
--- a/sys/src/cmd/ndb/mkfile	Wed Aug 26 21:17:17 2020 -0700
+++ b/sys/src/cmd/ndb/mkfile	Fri Aug 28 20:55:56 2020 -0700
@@ -13,6 +13,8 @@
 	dnstcp\
 	dnsdebug\
 	dnsgetip\
+	registry\
+	regquery\
 	ipquery\
 	inform\
 
@@ -49,6 +51,9 @@
 $O.cs: cs.$O
 	$LD -o $target $prereq
 
+$O.registry: registry.$O reglookup.$O
+	$LD -o $target $prereq
+
 $O.testipinfo: testipinfo.$O ipinfo.$O
 	$LD -o $target $prereq
 
diff -r 8127893f53b1 sys/src/cmd/ndb/registry.c
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/sys/src/cmd/ndb/registry.c	Fri Aug 28 20:55:56 2020 -0700
@@ -0,0 +1,896 @@
+#include <u.h>
+#include <libc.h>
+#include <auth.h>
+#include <fcall.h>
+#include <bio.h>
+#include <ip.h>
+#include "dns.h"
+
+enum
+{
+	Maxrequest=		1024,
+	Maxreply=		8192,
+	Maxrecords=		192,
+	Maxfdata=		8192,
+
+	Qdir=			0,
+	Qreg=			1,
+};
+
+typedef struct Mfile	Mfile;
+typedef struct Job	Job;
+typedef struct Records  Records;
+
+struct Mfile
+{
+	Mfile	*next;
+
+	char	*user;
+	Qid	qid;
+	int	fid;
+
+	char	reply[Maxreply];
+	ushort	rr[Maxrecords]; /* offset of record */
+	ushort	nrr;		/* number of records */	
+};
+
+/*
+ *  active requests
+ */
+struct Job
+{
+	Job	*next;
+	int	flushed;
+	Fcall	request;
+	Fcall	reply;
+};
+Lock	joblock;
+Job	*joblist;
+
+struct {
+	Lock;
+	Mfile	*inuse;		/* active mfile's */
+} mfalloc;
+
+Service *registry;
+int	vers;
+int	debug;
+char	*dbfile = "/lib/ndb/registry";
+char	*reguser;
+char	mntpt[Maxpath];
+int	mfd[2];
+char	*logfile = "registry";
+
+void	rversion(Job*);
+void	rflush(Job*);
+void	rattach(Job*, Mfile*);
+char*	rwalk(Job*, Mfile*);
+void	ropen(Job*, Mfile*);
+void	rcreate(Job*, Mfile*);
+void	rread(Job*, Mfile*);
+void	rwrite(Job*, Mfile*);
+void	rclunk(Job*, Mfile*);
+void	rremove(Job*, Mfile*);
+void	rstat(Job*, Mfile*);
+void	rwstat(Job*, Mfile*);
+void	rauth(Job*);
+void	mountinit(char*, char*);
+void	setext(char*, int, char*);
+void	io(void);
+
+char*	addsrv(char*);
+char*	rmsrv(char*);
+char*	updatesrv(char*);
+void	refresh(void);
+void	regdump(char*);
+void	sendmsg(Job*, char*);
+
+int	scanfmt(Fmt*);
+int	srvfmt(Fmt*);
+int	dumpfmt(Fmt*);
+
+static char* query(Job*, Mfile*, char*, int);
+
+void
+usage(void)
+{
+	fprint(2, "usage: %s [-xr] [-f ndb-file]\n", argv0);
+	exits("usage");
+}
+
+void
+main(int argc, char* argv[])
+{
+	char servefile[Maxpath], ext[Maxpath];
+	Dir *dir;
+	
+	setnetmtpt(mntpt, sizeof mntpt, nil);
+	ext[0] = 0;
+
+	ARGBEGIN{
+	case 'd':
+		debug = 1;
+		break;
+	case 'f':
+		dbfile = EARGF(usage());
+		break;
+	case 'x':
+		setnetmtpt(mntpt, sizeof mntpt, EARGF(usage()));
+		setext(ext, sizeof ext, mntpt);
+		break;
+	} ARGEND;
+	if(argc != 0)
+		usage();
+    
+	rfork(RFREND|RFNOTEG);
+
+	fmtinstall('F', fcallfmt);
+	fmtinstall('G', srvfmt);
+	fmtinstall('N', scanfmt);
+	fmtinstall('D', dumpfmt);
+
+	reglog("starting registry on %s", mntpt);
+
+	if(openregistry())
+		sysfatal("unable to open db file");
+
+	reguser = estrdup(getuser());
+	snprint(servefile, sizeof servefile, "#s/registry%s", ext);
+	
+	dir = dirstat(servefile);
+	if (dir)
+		sysfatal("%s exists; another registry instance is running", servefile);
+	free(dir);
+
+	mountinit(servefile, mntpt);
+	reg2cache();
+	io();
+
+	_exits(0);
+}
+
+void
+setext(char *ext, int n, char *p)
+{
+	int i, c;
+
+	n--;
+	for(i = 0; i < n; i++){
+		c = p[i];
+		if(c == 0)
+			break;
+		if(c == '/')
+			c = '_';
+		ext[i] = c;
+	}
+	ext[i] = 0;
+}
+
+void
+mountinit(char *service, char *mntpt)
+{
+	int f;
+	int p[2];
+	char buf[32];
+
+	if(pipe(p) < 0)
+		sysfatal("pipe failed: %r");
+
+	/*
+	 *  make a /srv/registry
+	 */
+	if((f = create(service, OWRITE|ORCLOSE, 0666)) < 0)
+		sysfatal("create %s failed: %r", service);
+	snprint(buf, sizeof buf, "%d", p[1]);
+	if(write(f, buf, strlen(buf)) != strlen(buf))
+		sysfatal("write %s failed: %r", service);
+
+	/* copy namespace to avoid a deadlock */
+	switch(rfork(RFFDG|RFPROC|RFNAMEG)){
+	case 0:			/* child: start main proc */
+		close(p[1]);
+		procsetname("%s", mntpt);
+		break;
+	case -1:
+		sysfatal("fork failed: %r");
+	default:		/* parent: make /srv/registry, mount it, exit */
+		close(p[0]);
+
+		/*
+		 *  put ourselves into the file system
+		 */
+		if(mount(p[1], -1, mntpt, MAFTER, "") < 0)
+			fprint(2, "registry mount failed: %r\n");
+		_exits(0);
+	}
+	mfd[0] = mfd[1] = p[0];
+}
+
+Mfile*
+newfid(int fid, int needunused)
+{
+	Mfile *mf;
+
+	lock(&mfalloc);
+	for(mf = mfalloc.inuse; mf != nil; mf = mf->next)
+		if(mf->fid == fid){
+			unlock(&mfalloc);
+			if(needunused)
+				return nil;
+			return mf;
+		}
+	mf = emalloc(sizeof(*mf));
+	mf->fid = fid;
+	mf->qid.vers = vers;
+	mf->qid.type = QTDIR;
+	mf->qid.path = 0LL;
+	mf->user = estrdup(reguser);
+	mf->next = mfalloc.inuse;
+	mfalloc.inuse = mf;
+	unlock(&mfalloc);
+	return mf;
+}
+
+void
+freefid(Mfile *mf)
+{
+	Mfile **l;
+
+	lock(&mfalloc);
+	for(l = &mfalloc.inuse; *l != nil; l = &(*l)->next)
+		if(*l == mf){
+			*l = mf->next;
+			free(mf->user);
+			memset(mf, 0, sizeof *mf);	/* cause trouble */
+			free(mf);
+			unlock(&mfalloc);
+			return;
+		}
+	unlock(&mfalloc);
+	sysfatal("freeing unused fid");
+}
+
+Mfile*
+copyfid(Mfile *mf, int fid)
+{
+	Mfile *nmf;
+
+	nmf = newfid(fid, 1);
+	if(nmf == nil)
+		return nil;
+	nmf->fid = fid;
+	free(nmf->user);
+	nmf->user = estrdup(mf->user);
+	nmf->qid.type = mf->qid.type;
+	nmf->qid.path = mf->qid.path;
+	nmf->qid.vers = vers++;
+	return nmf;
+}
+
+Job*
+newjob(void)
+{
+	Job *job;
+
+	job = emalloc(sizeof *job);
+	lock(&joblock);
+	job->next = joblist;
+	joblist = job;
+	job->request.tag = -1;
+	unlock(&joblock);
+	return job;
+}
+
+void
+freejob(Job *job)
+{
+	Job **l;
+
+	lock(&joblock);
+	for(l = &joblist; *l; l = &(*l)->next)
+		if(*l == job){
+			*l = job->next;
+			memset(job, 0, sizeof *job);	/* cause trouble */
+			free(job);
+			break;
+		}
+	unlock(&joblock);
+}
+
+void
+flushjob(int tag)
+{
+	Job *job;
+
+	lock(&joblock);
+	for(job = joblist; job; job = job->next)
+		if(job->request.tag == tag && job->request.type != Tflush){
+			job->flushed = 1;
+			break;
+		}
+	unlock(&joblock);
+}
+
+void
+io(void)
+{
+	long n;
+	Mfile *mf;
+	uchar mdata[IOHDRSZ + Maxfdata];
+	Job *job;
+
+	while((n = read9pmsg(mfd[0], mdata, sizeof mdata)) != 0){
+		if(n < 0){
+			syslog(1, logfile, "error reading 9P from %s: %r", mntpt);
+			break;
+		}
+
+		job = newjob();
+		if(convM2S(mdata, n, &job->request) != n){
+			reglog("format error %ux %ux %ux %ux %ux",
+				mdata[0], mdata[1], mdata[2], mdata[3], mdata[4]);
+			freejob(job);
+			break;
+		}
+		mf = newfid(job->request.fid, 0);
+		if(debug)
+			reglog("%F", &job->request);
+
+		switch(job->request.type){
+		default:
+			warning("unknown request type %d", job->request.type);
+			break;
+		case Tversion:
+			rversion(job);
+			break;
+		case Tauth:
+			rauth(job);
+			break;
+		case Tflush:
+			rflush(job);
+			break;
+		case Tattach:
+			rattach(job, mf);
+			break;
+		case Twalk:
+			rwalk(job, mf);
+			break;
+		case Topen:
+			ropen(job, mf);
+			break;
+		case Tcreate:
+			rcreate(job, mf);
+			break;
+		case Tread:
+			rread(job, mf);
+			break;
+		case Twrite:
+			rwrite(job, mf);
+			break;
+		case Tclunk:
+			rclunk(job, mf);
+			break;
+		case Tremove:
+			rremove(job, mf);
+			break;
+		case Tstat:
+			rstat(job, mf);
+			break;
+		case Twstat:
+			rwstat(job, mf);
+			break;
+		}
+
+		freejob(job);
+	}
+}
+
+void
+rversion(Job *job)
+{
+	if(job->request.msize > IOHDRSZ + Maxfdata)
+		job->reply.msize = IOHDRSZ + Maxfdata;
+	else
+		job->reply.msize = job->request.msize;
+	job->reply.version = "9P2000";
+	if(strncmp(job->request.version, "9P", 2) != 0)
+		job->reply.version = "unknown";
+	sendmsg(job, nil);
+}
+
+void
+rauth(Job *job)
+{
+	sendmsg(job, "registry: authentication not required");
+}
+
+void
+rflush(Job *job)
+{
+	flushjob(job->request.oldtag);
+	sendmsg(job, 0);
+}
+
+void
+rattach(Job *job, Mfile *mf)
+{
+	if(mf->user != nil)
+		free(mf->user);
+	mf->user = estrdup(job->request.uname);
+	mf->qid.vers = vers++;
+	mf->qid.type = QTDIR;
+	mf->qid.path = 0LL;
+	job->reply.qid = mf->qid;
+	sendmsg(job, 0);
+}
+
+char*
+rwalk(Job *job, Mfile *mf)
+{
+	int i, nelems;
+	char *err;
+	char **elems;
+	Mfile *nmf;
+	Qid qid;
+
+	err = 0;
+	nmf = nil;
+	elems = job->request.wname;
+	nelems = job->request.nwname;
+	job->reply.nwqid = 0;
+
+	if(job->request.newfid != job->request.fid){
+		/* clone fid */
+		nmf = copyfid(mf, job->request.newfid);
+		if(nmf == nil){
+			err = "clone bad newfid";
+			goto send;
+		}
+		mf = nmf;
+	}
+	/* else nmf will be nil */
+
+	qid = mf->qid;
+	if(nelems > 0){
+		/* walk fid */
+		for(i=0; i<nelems && i<MAXWELEM; i++){
+			if((qid.type & QTDIR) == 0){
+				err = "not a directory";
+				break;
+			}
+			if(strcmp(elems[i], "..") == 0 || strcmp(elems[i], ".") == 0){
+				qid.type = QTDIR;
+				qid.path = Qdir;
+    Found:
+				job->reply.wqid[i] = qid;
+				job->reply.nwqid++;
+				continue;
+			}
+			if(strcmp(elems[i], "registry") == 0){
+				qid.type = QTFILE;
+				qid.path = Qreg;
+				goto Found;
+			}
+			err = "file does not exist";
+			break;
+		}
+	}
+
+    send:
+	if(nmf != nil && (err!=nil || job->reply.nwqid<nelems))
+		freefid(nmf);
+	if(err == nil)
+		mf->qid = qid;
+	sendmsg(job, err);
+	return err;
+}
+
+void
+ropen(Job *job, Mfile *mf)
+{
+	int mode;
+	char *err;
+
+	err = 0;
+	mode = job->request.mode;
+	if(mf->qid.type & QTDIR)
+		if(mode)
+			err = "permission denied";
+	job->reply.qid = mf->qid;
+	job->reply.iounit = 0;
+	sendmsg(job, err);
+}
+
+void
+rcreate(Job *job, Mfile *mf)
+{
+	USED(mf);
+	sendmsg(job, "creation permission denied");
+}
+
+void rclunk(Job *job, Mfile *mf)
+{
+	freefid(mf);
+	sendmsg(job, 0);
+}
+
+void
+rremove(Job *job, Mfile *mf)
+{
+	USED(mf);
+	sendmsg(job, "remove permission denied");
+}
+
+void 
+rread(Job *job, Mfile *mf)
+{
+	int i, n;
+	long clock;
+	ulong cnt;
+	vlong off;
+	char *err;
+	uchar buf[Maxfdata];
+	Dir dir;
+
+	n = 0;
+	err = nil;
+	off = job->request.offset;
+	cnt = job->request.count;
+	*buf = '\0';
+	job->reply.data = (char*)buf;
+	if(mf->qid.type & QTDIR){
+		clock = time(nil);
+		if(off == 0){
+			memset(&dir, 0, sizeof dir);
+			dir.name = "registry";
+			dir.qid.type = QTFILE;
+			dir.qid.vers = vers;
+			dir.qid.path = Qreg;
+			dir.mode = 0666;
+			dir.length = 0;
+			dir.uid = dir.gid = dir.muid = mf->user;
+			dir.atime = dir.mtime = clock;
+			n = convD2M(&dir, buf, sizeof buf);
+		}
+	} else if (off < 0)
+		err = "negative read offset";
+	else {
+		for(i = 1; i < mf->nrr; i++)
+			if(mf->rr[i] > off)
+				break;
+		if(i <= mf->nrr){
+			if(off + cnt > mf->rr[i])
+				n = mf->rr[i] - off;
+			else
+				n = cnt;
+			assert(n >= 0);
+			job->reply.data = mf->reply + off;
+		}
+	}
+	job->reply.count = n;
+	sendmsg(job, err);	
+}
+
+void 
+rwrite(Job *job, Mfile *mf)
+{
+	int send, pipe2rc;
+	ulong cnt;	
+	char *err, *atype;
+	char errbuf[ERRMAX];
+	
+	err = nil;
+	cnt = job->request.count;
+	send = 1;
+	if(mf->qid.type & QTDIR)
+		err = "can't write directory";
+	else if (job->request.offset != 0)
+		err = "writing at non-zero offset";
+	else if (cnt >= Maxrequest)
+		err = "request too long";
+	else
+		send = 0;
+	if(send)
+		goto send;
+
+	job->request.data[cnt] = 0;
+	if(cnt > 0 && job->request.data[cnt-1] == '\n')
+		job->request.data[cnt-1] = 0;
+
+	if(strcmp(mf->user, "none") == 0 || strcmp(mf->user, reguser) != 0)
+		goto query; /* We don't want remote clients to modify our local */
+
+	/*
+	 * special commands
+	 */
+	send = 1;
+	if(strcmp(job->request.data, "debug")==0)
+		debug ^= 1;
+	else if(strcmp(job->request.data, "dump")==0)
+		regdump("/lib/ndb/regdump");
+	else if (strcmp(job->request.data, "refresh")==0)
+		refresh();
+	else if (strncmp(job->request.data, "add ", 4)==0)
+		err = addsrv(job->request.data + 4);
+	else if (strncmp(job->request.data, "rm ", 3)==0)
+		err = rmsrv(job->request.data + 3);
+	else if (strncmp(job->request.data, "update ", 7)==0)
+		err = updatesrv(job->request.data + 7);
+	else
+		send = 0;
+	if (send)
+		goto send;
+
+query:
+	/*
+	 *	kill previous reply
+	 */
+	mf->nrr = 0;
+	mf->rr[0] = 0;
+	pipe2rc = 0;
+	
+	atype = strchr(job->request.data, ' ');
+	if(atype == 0){
+		snprint(errbuf, sizeof errbuf, "illegal request %s", job->request.data);
+		err = errbuf;
+		goto send;
+	} else
+		*atype++ = 0;
+
+	if(strcmp(atype, "srv") == 0)
+		pipe2rc++;
+	else if(strcmp(atype, "scan") != 0){
+		snprint(errbuf, sizeof errbuf, "unknown query %s", atype);
+		err = errbuf;
+		goto send;
+	}
+
+	err = query(job, mf,job->request.data, pipe2rc);
+send:
+	job->reply.count = cnt;
+	sendmsg(job, err);
+}
+
+void
+rstat(Job *job, Mfile *mf)
+{
+	Dir dir;
+	uchar buf[IOHDRSZ+Maxfdata];
+	
+	memset(&dir, 0, sizeof dir);
+	if(mf->qid.type & QTDIR){
+		dir.name = ".";
+		dir.mode = DMDIR|0555;
+	}else{
+		dir.name = "registry";
+		dir.mode = 0666;
+	}
+	dir.qid = mf->qid;
+	dir.length = 0;
+	dir.uid = dir.gid = dir.muid = mf->user;
+	dir.atime = dir.mtime = time(nil);
+	job->reply.nstat = convD2M(&dir, buf, sizeof buf);
+	job->reply.stat = buf;
+	sendmsg(job, 0);
+}
+
+void
+rwstat(Job *job, Mfile *mf)
+{
+	USED(mf);
+	sendmsg(job, "wstat permission denied");
+}
+
+static char *
+query(Job *job, Mfile *mf, char *p, int pipe2rc)
+{
+	int n;
+
+	Service *c;
+	lock(&joblock);
+	if(!job->flushed){
+		n = 0;
+		mf->nrr = 0;
+		for(c = registry; c && n < Maxreply; c = c->next)
+			if((strncmp(p, c->labl, strlen(p))==0) || (strcmp(p, "all")==0)){
+				mf->rr[mf->nrr++] = n;
+				if(pipe2rc)
+					n += snprint(mf->reply+n, Maxreply-n, "%G", c);
+				else
+					n += snprint(mf->reply+n, Maxreply-n, "%N", c);
+			}
+		mf->rr[mf->nrr] = n;
+	}
+	unlock(&joblock);
+	return nil;
+}
+
+void
+sendmsg(Job *job, char *err)
+{
+	int n;
+	uchar mdata[IOHDRSZ+Maxfdata];
+	char ename[ERRMAX];
+
+	if(err){
+		job->reply.type = Rerror;
+		snprint(ename, sizeof ename, "registry: %s", err);
+		job->reply.ename = ename;
+	}else
+		job->reply.type = job->request.type+1;
+	job->reply.tag = job->request.tag;
+	n = convS2M(&job->reply, mdata, sizeof mdata);
+	if(n == 0){
+		warning("sendmsg convS2M of %F returns 0", &job->reply);
+		abort();
+	}
+	lock(&joblock);
+	if(job->flushed == 0)
+		if(write(mfd[1], mdata, n)!=n)
+			sysfatal("mount write");
+	unlock(&joblock);
+	if(debug)
+		reglog("%F %d", &job->reply, n);
+}
+
+void
+regdump(char *file)
+{
+	Service *rp;
+	int fd;
+
+	fd = create(file, OWRITE, 0666);
+	if(fd < 0)
+		return;
+	lock(&mfalloc);
+	for(rp = registry; rp; rp = rp->next)
+		fprint(fd, "%D\n\n", rp);
+	unlock(&mfalloc);
+	close(fd);
+}
+
+void
+refresh(void)
+{	
+	Service *c;
+	char dial[Maxdial];
+
+	for(c = registry; c; c = c->next){
+		/* Don't remove the ones we've added since startup */
+		if(!c->perm)
+			continue;
+		snprint(dial, Maxdial, "%s!%s!%s", c->trns, c->host, c->port);
+		rmsrv(dial);
+		/* Reset so we don't have messy loops */
+		c = registry;
+	}
+	reg2cache();
+}
+
+char *
+addsrv(char *args)
+{
+	if(debug)
+		reglog("Adding entry: %s", args);
+
+	return rstr2cache(args, 0);
+}
+
+char *
+rmsrv(char *args)
+{
+	if(debug)
+		reglog("Removing entry: %s", args);
+
+	return rstrdtch(args);
+}
+
+char *
+updatesrv(char *args)
+{
+	if(debug)
+		reglog("Updating entry: %s", args);
+
+	return rstrupdt(args);
+}
+
+void
+warning(char *fmt, ...)
+{
+	char regerr[256];
+	va_list arg;
+
+	va_start(arg, fmt);
+	vseprint(regerr, regerr+sizeof(regerr), fmt, arg);
+	va_end(arg);
+	syslog(1, logfile, regerr);
+}
+
+void
+reglog(char *fmt, ...)
+{
+	char regerr[256];
+	va_list arg;
+	
+	va_start(arg, fmt);
+	vseprint(regerr, regerr+sizeof(regerr), fmt, arg);
+	va_end(arg);
+	syslog(0, logfile, regerr);
+}
+
+void*
+emalloc(int size)
+{
+	void *x;
+
+	x = malloc(size);
+	if(x == nil)
+		sysfatal("out of memory");
+	memset(x, 0, size);
+	return x;
+}
+
+char*
+estrdup(char *s)
+{
+	int size;
+	char *p;
+
+	size = strlen(s);
+	p = malloc(size+1);
+	if(p == nil)
+		sysfatal("out of memory");
+	memmove(p, s, size);
+	p[size] = 0;
+	return p;
+}
+
+int
+srvfmt(Fmt *f)
+{
+	Service *r;
+	char mf[Maxpath+1], auth[7];
+	
+	r = va_arg(f->args, Service*);
+	mf[0] = 0;
+	auth[0] = 0;
+		
+	if(strcmp(r->mtpt, "")!= 0)
+		snprint(mf, sizeof(r->mtpt)+1, " %s", r->mtpt);
+
+	if(strcmp(r->auth, "none")==0)
+		snprint(auth, 4, "srv");
+	else
+		snprint(auth, 7, "srvtls");
+
+	return fmtprint(f, "%s %s!%s!%s '%s'%s",
+		auth, r->trns, r->host, r->port, r->labl, mf);
+}
+
+int
+scanfmt(Fmt *f)
+{
+	Service *r;
+	char mf[Maxpath+6]; /* pad for our tuple attrs */
+
+	mf[0] = 0;
+	r = va_arg(f->args, Service*);
+	if(strcmp(r->mtpt, "")!=0)
+		snprint(mf, sizeof(r->mtpt)+6, " mtpt=%s", r->mtpt);
+	return fmtprint(f, "service=%s!%s!%s label='%s' auth=%s%s",
+		r->trns, r->host, r->port, r->labl, r->auth, mf);
+}
+
+int
+dumpfmt(Fmt *f)
+{
+	Service *r;
+	char mf[Maxpath+7]; /* pad for our tuple attrs */
+
+	r = va_arg(f->args, Service*);
+	if(r->mtpt != 0)
+		snprint(mf, sizeof(r->mtpt) + 7, "\n\tmtpt=%s", r->mtpt);
+	return fmtprint(f, "service=%s!%s!%s\n\tlabel=%s\n\tauth=%s%s",
+		r->trns, r->host, r->port, r->labl, r->auth, mf);
+}
+
diff -r 8127893f53b1 sys/src/cmd/ndb/reglookup.c
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/sys/src/cmd/ndb/reglookup.c	Fri Aug 28 20:55:56 2020 -0700
@@ -0,0 +1,204 @@
+#include <u.h>
+#include <libc.h>
+#include <bio.h>
+#include <ndb.h>
+#include "dns.h"
+
+static Ndb	*db;
+static QLock	dblock;
+
+int
+openregistry(void)
+{
+	if(db != nil)
+		return 0;
+
+	db = ndbopen(dbfile);
+	return db!=nil ? 0: -1;
+}
+
+static void
+attach(Service* svc, int persist)
+{
+	svc->perm = !!persist;
+
+	if(registry != nil){
+		svc->next = registry;
+	}
+
+	svc->next = registry;
+	registry = svc;
+}
+
+static char*
+detach(char *dial)
+{
+	Service *c, *last = 0;
+	char buf[Maxdial]; /* trns is capped at 16, port 8 */
+
+	for(c = registry; c; c = c->next){
+		snprint(buf, Maxdial, "%s!%s!%s", c->trns, c->host, c->port);
+		if(strcmp(buf, dial)==0){
+			if(last == 0)
+				registry = c->next;
+			else
+				last->next = c->next;
+			free(c);
+			return 0;
+		}
+		last = c;
+	}
+
+	return "found no matching service";
+}
+
+static void
+host2svc(Service *svc, char *dial)
+{
+	int n;
+
+	/* 
+	 * entry host=tcp!mything!9fs
+	 * for now, tokenize but we should allow short strings
+         */
+	n = strcspn(dial, "!");
+	if(n < 1)
+		strcpy(svc->trns, "tcp");
+	else
+		strecpy(svc->trns, svc->trns+n+1, dial);
+	dial = dial + n + 1;
+
+	n = strcspn(dial, "!");
+	strecpy(svc->host, svc->host+n+1, dial);
+
+	dial = dial + n + 1;
+	if(sizeof(dial) < 1)
+		strcpy(svc->port, "9fs");
+	else if(sizeof(dial) > 8)
+		/* If this starts happening, we should bump the number */
+		strecpy(svc->port, svc->port + 8, dial);
+	else
+		strcpy(svc->port, dial);
+}
+
+static void
+dbtuple2cache(Ndbtuple *t, int persist)
+{
+	Ndbtuple *et, *nt;
+	Service *svc;
+
+
+	for(et = t; et; et = et->entry)
+		if(strncmp(et->attr, "serv", 4)==0){
+			svc = emalloc(sizeof(*svc));
+			host2svc(svc, et->val);
+			for(nt = et->entry; nt; nt = nt->entry)
+				if(strcmp(nt->attr, "label")==0)
+					strecpy(svc->labl, svc->labl+Maxmdns, nt->val);
+				else if(strcmp(nt->attr, "auth")==0)
+					strecpy(svc->auth, svc->auth+Maxauth, nt->val);
+				else if(strcmp(nt->attr, "mtpt")==0)
+					strecpy(svc->mtpt, svc->mtpt+Maxpath, nt->val);
+			attach(svc, persist);
+		};
+}
+
+static void
+dbfile2cache(Ndb *db)
+{
+	Ndbtuple *t;
+
+	if(debug)
+		reglog("reading %s", db->file);
+	Bseek(&db->b, 0, 0);
+	while(t = ndbparse(db)){
+		dbtuple2cache(t, 1);
+		ndbfree(t);
+	}
+
+
+}
+
+/* For now, we expect properly formatted strings */
+char*
+rstr2cache(char *entry, int persist)
+{
+	Service *svc;
+	char *args[7];
+	
+	int i, n;
+
+	n = tokenize(entry, args, 7);
+
+	svc = emalloc(sizeof(*svc));
+	host2svc(svc, estrdup(args[0]));
+
+	for(i = 1; i < n - 1; i++)
+		if(strcmp(args[i], "label")==0)	
+			strecpy(svc->labl, svc->labl+Maxmdns, args[++i]);
+		else if(strcmp(args[i], "auth")==0)
+			strecpy(svc->auth, svc->auth+Maxauth, args[++i]);
+		else if(strcmp(args[i], "mtpt")==0)
+			strecpy(svc->mtpt, svc->mtpt+Maxpath, args[++i]);
+	attach(svc, persist);
+	return 0;
+}
+
+char*
+rstrdtch(char *svc)
+{
+	return detach(svc);
+}
+
+/* e.g. update tcp!foo!9fs label newlabel */
+char*
+rstrupdt(char *entry)
+{
+	Service *c, *svc = 0;
+	char *args[7], buf[Maxdial];
+	int i, n;
+
+	n = tokenize(entry, args, 7);
+
+	/* Find our service */
+	for(c = registry; c; c = c->next){
+		snprint(buf, Maxdial, "%s!%s!%s", c->trns, c->host, c->port);
+		if(strcmp(buf, args[0])==0){
+			svc = c;
+			break;
+		}
+	}
+	
+	if(svc == 0)
+		return "found no matching service";
+
+	for(i = 1; i < n - 1; i++)
+		if(strcmp(args[i], "label")==0)
+			strecpy(svc->labl, svc->labl+Maxmdns, args[++i]);
+		else if(strcmp(args[i], "auth")==0)
+			strecpy(svc->auth, svc->auth+Maxauth, args[++i]);
+		else if(strcmp(args[i], "mtpt")==0)
+			strecpy(svc->mtpt, svc->mtpt+Maxpath, args[++i]);
+
+	return 0;
+}
+
+void
+reg2cache(void)
+{
+	Ndb *ndb;
+	
+	qlock(&dblock);
+	if(openregistry() < 0){
+		qunlock(&dblock);
+		return;
+	}
+	
+	if(debug)
+		syslog(0, logfile, "building cache from db");
+			
+	for(ndb = db; ndb; ndb = ndb->next)
+		dbfile2cache(ndb);
+
+	qunlock(&dblock);
+}
diff -r 8127893f53b1 sys/src/cmd/ndb/regquery.c
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/sys/src/cmd/ndb/regquery.c	Fri Aug 28 20:55:56 2020 -0700
@@ -0,0 +1,69 @@
+#include <u.h>
+#include <libc.h>
+#include <bio.h>
+#include <ctype.h>
+#include <ndb.h>
+#include "dns.h"
+#include "ip.h"
+
+void
+usage(void)
+{
+	fprint(2, "usage: regquery [-s] [-f registry] query\n");
+	exits("usage");
+}
+
+static void
+queryregistry(int fd, char *line, int n)
+{
+	char buf[8192+1];
+
+	seek(fd, 0, 0);
+	write(fd, line, n);
+
+	seek(fd, 0, 0);
+	while((n = read(fd, buf, sizeof(buf)-1)) > 0){
+		buf[n++] = '\n';
+		write(1, buf, n);
+	}
+}
+
+static void
+query(int fd, char *q, int pipe2rc)
+{
+	char arg[260];
+
+	if(strlen(q) > 255)
+		sysfatal("query too long");
+
+	sprint(arg, "%s %s", q, (pipe2rc) ? "srv":"scan");
+	queryregistry(fd, arg, sizeof(arg));
+}
+
+void
+main(int argc, char *argv[])
+{
+	int fd, pipe2rc = 0;
+	char *rst  = "/net/registry";
+
+	ARGBEGIN {
+	case 's':
+		pipe2rc++;
+		break;
+	case 'f':
+		rst = EARGF(usage());
+		break;
+	default:
+		usage();
+	} ARGEND;
+
+	if(argc != 1)
+		usage();
+
+	fd = open(rst, ORDWR);
+	if(fd < 0)
+		sysfatal("can't open %s: %r", rst);
+
+	query(fd, argv[0], pipe2rc);
+	exits(0);
+}
