shithub: 9ferno

Download patch

ref: 5e0faca2085cecc4298b7a898fe1e549818c9b0c
parent: 84b2922214d8d61e6dfd5b28542bcfdb56bceba2
author: 9ferno <gophone2015@gmail.com>
date: Fri Aug 13 08:24:53 EDT 2021

added dhcpd

merged https://github.com/mjl-/dhcpd authored by Mechiel Lukkien

--- /dev/null	Mon Oct 18 12:05:45 2021
+++ b/appl/cmd/ip/dhcpd.b	Fri Aug 13 08:24:53 2021
@@ -0,0 +1,915 @@
+implement Dhcpd;
+
+include "sys.m";
+	sys: Sys;
+	sprint: import sys;
+include "draw.m";
+include "arg.m";
+include "bufio.m";
+	bufio: Bufio;
+	Iobuf: import bufio;
+include "string.m";
+	str: String;
+include "daytime.m";
+	daytime: Daytime;
+include "attrdb.m";
+	attrdb: Attrdb;
+	Attr, Db, Dbentry: import attrdb;
+include "ip.m";
+	ip: IP;
+	IPaddr, Udphdr: import ip;
+include "ether.m";
+	ether: Ether;
+include "lists.m";
+	lists: Lists;
+include "encoding.m";
+	base16: Encoding;
+include "dhcpd.m";
+	dhcp: Dhcpserver;
+	Dhcpmsg, Opt: import dhcp;
+include "ipval.m";
+	ipval: Ipval;
+
+Dhcpd: module
+{
+	init: fn(nil: ref Draw->Context, args: list of string);
+};
+
+dflag: int;
+sflag: int;
+
+siaddr: IPaddr;
+sysname: string;
+net: string;
+ndb: ref Db;
+ndbfile := "/lib/ndb/local";
+statedir := "/services/dhcpd";
+leasetime := 24*3600;
+bootpfd: ref Sys->FD;
+
+
+Range: adt {
+	ip:	IPaddr;
+	n:	int;
+};
+ranges: list of Range;
+
+Addr: adt {
+	ip:		IPaddr;
+	pick {
+	Fixed =>
+	Dynamic =>
+		hwaddr:		array of byte;
+		clientid:	array of byte;
+		leasestart:	int;
+		leasetime:	int;	# 0 means unassigned
+		hostname:	string;
+	}
+};
+
+pool: array of ref Addr.Dynamic;
+pooloff: int;
+
+
+# dhcp options -> ndb attributes
+optattrs := array[] of {
+dhcp->Osubnetmask	=> "ipmask",
+dhcp->Orouters		=> "ipgw",
+dhcp->Odns		=> "dns",
+dhcp->Ohostname		=> "sys",
+dhcp->Odomainname	=> "dnsdomain",
+dhcp->Orootpath		=> "rootpath",
+#dhcp->Obroadcast	=> "ipbroadcast",
+
+dhcp->Oleasetime	=> "leasetime",
+#dhcp->Ovendorclass	=> "dhcpvendorclass",
+dhcp->Otftpservername	=> "tftp",
+dhcp->Obootfile		=> "bootf",
+};
+
+
+init(nil: ref Draw->Context, args: list of string)
+{
+	sys = load Sys Sys->PATH;
+	bufio = load Bufio Bufio->PATH;
+	arg := load Arg Arg->PATH;
+	str = load String String->PATH;
+	daytime = load Daytime Daytime->PATH;
+	attrdb = load Attrdb Attrdb->PATH;
+	attrdb->init();
+	ip = load IP IP->PATH;
+	ip->init();
+	ether = load Ether Ether->PATH;
+	ether->init();
+	lists = load Lists Lists->PATH;
+	base16 = load Encoding Encoding->BASE16PATH;
+	dhcp = load Dhcpserver Dhcpserver->PATH;
+	dhcp->init();
+	ipval = load Ipval Ipval->PATH;
+
+	arg->init(args);
+	arg->setusage(arg->progname()+" [-ds] [-f file] [-x network] [ipaddr n ...]");
+	while((o := arg->opt()) != 0)
+		case o {
+		'd' =>	dflag++;
+		's' =>	sflag++;
+		'x' =>	net = arg->earg();
+		'f' =>	ndbfile = arg->earg();
+		* =>	arg->usage();
+		}
+	args = arg->argv();
+	if(len args % 2 != 0)
+		arg->usage();
+	for(; args != nil; args = tl tl args)
+		addrange(hd args, hd tl args);
+	poolmake();
+	poolreadstate();
+	pooldump();
+
+	sys->pctl(Sys->FORKFD|Sys->FORKNS, nil);
+
+	if(!sflag) {
+		ndb = Db.open(ndbfile);
+		if(ndb == nil)
+			fail(sprint("db open %q: %r", ndbfile));
+
+		sysname = readfile("/dev/sysname");
+		if(sysname == nil)
+			fail("could not determine system name, /dev/sysname");
+
+		(e, nil) := ndb.findbyattr(nil, "sys", sysname, "ip");
+		if(e != nil)
+			ipaddr := e.findfirst("ip");
+		ok: int;
+		(ok, siaddr) = IPaddr.parse(ipaddr);
+		if(ok < 0 || !siaddr.isv4() || siaddr.eq(ip->v4noaddr))
+			fail(sprint("could not determine ip for system name %q", sysname));
+		say("local ip address is "+siaddr.text());
+	}
+
+	addr := net+"udp!*!bootp";
+	(ok, c) := sys->announce(addr);
+	if(ok < 0)
+		fail(sprint("announce %s: %r", addr));
+	say("announced "+addr);
+	if(sys->fprint(c.cfd, "headers") < 0)
+		fail(sprint("setting headers mode: %r"));
+
+	path := c.dir+"/data";
+	bootpfd = sys->open(path, sys->ORDWR);
+	if(bootpfd == nil)
+		fail(sprint("open %s: %r", path));
+	spawn server();
+}
+
+addrange(ipaddr, ns: string)
+{
+	# parse addr, generate end address
+	(ok, ipa) := IPaddr.parse(ipaddr);
+	if(ok < 0 || !ipa.isv4() || ipa.eq(ip->v4noaddr) || ipa.eq(ip->noaddr) || ipa.ismulticast())
+		fail(sprint("bad ip %q", ipaddr));
+
+	(n, rem) := str->toint(ns, 10);
+	if(rem != nil || n <= 0)
+		fail(sprint("bad count %q", ns));
+	start := ipa.v4();
+	if(int start[3]+n > 255)
+		fail(sprint("range extends past subnet, %q %q", ipaddr, ns));
+
+	# check that there is no address overlap
+	for(l := ranges; l != nil; l = tl l) {
+		r := hd l;
+		rstart := r.ip.v4();
+		if(!eq(start[:3], rstart[:3]))
+			continue;
+		s := int start[3];
+		rs := int rstart[3];
+		e := s+n;
+		re := rs+r.n;
+		if(s <= rs && e >= re || s >= rs && s < re || e > rs && e < re)
+			fail(sprint("ranges overlap, %q %d with %q %d", r.ip.text(), r.n, ipaddr, n));
+	}
+
+	# record the new range
+	ranges = Range (ipa, n)::ranges;
+}
+
+# we order by longest unused.
+addrge(a, b: ref Addr.Dynamic): int
+{
+	if(a.leasetime == 0 && b.leasetime == 0) {
+		if(a.leasestart == b.leasestart)
+			return ipcmp(a.ip, b.ip) >= 0;
+		return a.leasestart >= b.leasestart;
+	}
+	if(a.leasetime == 0)
+		return 0;
+	if(b.leasetime == 0)
+		return 1;
+	return a.leasestart+a.leasetime >= b.leasestart+b.leasetime;
+}
+
+poolmake()
+{
+	la: list of ref Addr.Dynamic;
+	for(l := ranges; l != nil; l = tl l) {
+		r := hd l;
+		s := r.ip.v4();
+		for(i := 0; i < r.n; i++) {
+			ipa := IPaddr.newv4(s);
+			a := ref Addr.Dynamic (ipa, nil, nil, 0, 0, nil);
+			la = a::la;
+			s[3]++;
+		}
+	}
+	pool = l2a(la);
+	sort(pool, addrge);
+	pooloff = 0;
+}
+
+readstatefile(ipfile: string)
+{
+	(ok, ipa) := IPaddr.parse(ipfile);
+	if(ok < 0)
+		return;
+	a := poolfindip(ipa);
+	if(a == nil)
+		return;
+
+	path := sprint("%s/%s", statedir, ipfile);
+	s := readfile(path);
+	if(s == nil)
+		return warn(sprint("statefile %q: %r;  ignoring", path));
+
+	# hwaddr clientid leasestart leasetime hostname
+	t := str->unquoted(s);
+	if(len t != 5)
+		return warn(sprint("statefile %q: bad format, ignoring", path));
+
+	a.hwaddr = base16->dec(hd t);
+	t = tl t;
+	a.clientid = base16->dec(hd t);
+	t = tl t;
+	a.leasestart = int hd t;
+	t = tl t;
+	a.leasetime = int hd t;
+	t = tl t;
+	a.hostname = hd t;
+	t = tl t;
+}
+
+writestatefile(a: ref Addr.Dynamic)
+{
+	path := sprint("%s/%s", statedir, a.ip.text());
+	s := sprint("%q %q %d %d %q\n", hex(a.hwaddr), hex(a.clientid), a.leasestart, a.leasetime, a.hostname);
+	err := writefile(path, s);
+	if(err != nil)
+		warn(sprint("write %q: %s", path, err));
+}
+
+poolreadstate()
+{
+	fd := sys->open(statedir, Sys->OREAD);
+	if(fd == nil)
+		return warn(sprint("open %q: %r;  will not keep state", statedir));
+	for(;;) {
+		(n, d) := sys->dirread(fd);
+		if(n < 0)
+			return warn(sprint("dirread %q: %r;  state not read", statedir));
+		if(n == 0)
+			break;
+		for(i := 0; i < n; i++)
+			readstatefile(d[i].name);
+	}
+}
+
+pooldump()
+{
+	say("pool:");
+	for(i := 0; i < len pool; i++)
+		say(sprint("\t%s", pool[i].ip.text()));
+	say("end pool");
+}
+
+server()
+{
+	buf := array[2048] of byte;
+	for(;;) {
+		n := sys->read(bootpfd, buf, len buf);
+		if(n < 0)
+			return warn(sprint("read packet: %r"));
+		serve(buf[:n]);
+	}
+}
+
+serve(buf: array of byte)
+{
+	if(len buf < IP->Udphdrlen)
+		return say(sprint("short udp header: %d bytes", len buf));
+	hdr := Udphdr.unpack(buf, IP->Udphdrlen);
+	say(sprint("udp read, remote %q!%d, local %q!%d, ifcaddr %q", hdr.raddr.text(), hdr.rport, hdr.laddr.text(), hdr.lport, hdr.ifcaddr.text()));
+	buf = buf[IP->Udphdrlen:];
+
+	(im, err) := Dhcpmsg.unpack(buf);
+	if(err != nil)
+		return say("parsing dhcpmsg: "+err);
+	if(sflag || dflag >= 2)
+		say("-> "+im.text());
+	if(sflag)
+		return;
+
+	if(im.op != dhcp->Trequest)
+		return say("message is reply, not request;  ignoring");
+	if(im.htype != 1 || im.hlen != 6)
+		return say("hardware type not ether; ignoring");
+
+	if(ndb.changed() && ndb.reopen() < 0)
+		return warn(sprint("reopening ndb file: %r"));
+
+	om: ref Dhcpmsg;
+	t := dhcpmsgtype(im);
+	if(t < 0) {
+		say("bootp request");
+	} else {
+		say("dhcp request");
+		om = dhcpsrv(t, im);
+	}
+
+	if(om != nil) {
+		say("<- "+om.text());
+		obuf := om.pack();
+
+		ipdest := im.giaddr;
+		if(ipdest.eq(ip->v4noaddr)) {
+			if(im.flags & dhcp->Fbroadcast)
+				ipdest = ip->v4bcast;
+			else
+				ipdest = ip->v4bcast; # xxx
+		}
+
+		hdr.raddr = ipdest;
+		hdr.laddr = siaddr;
+		hdr.ifcaddr = siaddr;
+		hdr.rport = 68;
+		hdr.lport = 67;
+		pkt := array[ip->Udphdrlen+len obuf] of byte;
+		hdr.pack(pkt, ip->Udphdrlen);
+		pkt[ip->Udphdrlen:] = obuf;
+		say(sprint("udp write, remote %q!%d, local %q!%d, ifcaddr %q", hdr.raddr.text(), hdr.rport, hdr.laddr.text(), hdr.lport, hdr.ifcaddr.text()));
+		if(sys->write(bootpfd, pkt, len pkt) != len pkt)
+			warn(sprint("udp write: %r"));
+	}
+}
+
+dhcpmsgtype(m: ref Dhcpmsg): int
+{
+	if(g32(m.options) == dhcp->Moptions)
+		for(l := m.opts; l != nil; l = tl l)
+			if((hd l).code == dhcp->Odhcpmsgtype && len (hd l).v == 1)
+				return int (hd l).v[0];
+	return -1;
+}
+
+getoption(m: ref Dhcpmsg, code: int): array of byte
+{
+	for(l := m.opts; l != nil; l = tl l)
+		if((hd l).code == code)
+			return (hd l).v;
+	return nil;
+}
+
+getoptionip(m: ref Dhcpmsg, code: int): ref IPaddr
+{
+	v := getoption(m, code);
+	if(v != nil && len v == 4)
+		ipa := ref IPaddr.newv4(v);
+	return ipa;
+}
+
+ethertext0(hwaddr: array of byte): string
+{
+	return ether->text(hwaddr);
+}
+
+ethertext1(hwaddr: array of byte): string
+{
+	s := ether->text(hwaddr);
+	if(len s != 12)  # 6 bytes in hex, as mentioned by ether(2)
+		return s;
+	return sprint("%s:%s:%s:%s:%s:%s", s[0:2], s[2:4], s[4:6], s[6:8], s[8:10], s[10:12]);
+}
+
+etherip(hwaddr: array of byte): ref IPaddr
+{
+	(e, nil) := ndb.findbyattr(nil, "ether", ethertext0(hwaddr), "ip");
+	if(e == nil)
+		(e, nil) = ndb.findbyattr(nil, "ether", ethertext1(hwaddr), "ip");
+	if(e == nil)
+		return nil;
+	ipaddr := e.findfirst("ip");
+	if(ipaddr == nil)
+		return nil;
+	(ok, eipa) := IPaddr.parse(ipaddr);
+	if(ok < 0)
+		return nil;
+	return ref eipa;
+}
+
+None, Hit, Bad: con iota;
+etheripmatch(hwaddr: array of byte, ipa: IPaddr): int
+{
+	(e, nil) := ndb.findbyattr(nil, "ether", ether->text(hwaddr), "ip");
+	if(e == nil)
+		(e, nil) = ndb.findbyattr(nil, "ether", ethertext1(hwaddr), "ip");
+	if(e != nil)
+		ipaddr := e.findfirst("ip");
+	if(ipaddr == nil)
+		return None;
+	(ok, eipa) := IPaddr.parse(ipaddr);
+	if(ok < 0 || !ipa.eq(eipa))
+		return Bad;
+	return Hit;
+}
+
+leasematch(a: ref Addr.Dynamic, hwaddr, clientid: array of byte): int
+{
+	if(clientid != nil && a.clientid != nil)
+		return eq(clientid, a.clientid);
+	return eq(hwaddr, a.hwaddr);
+}
+
+poolfindip(ipa: IPaddr): ref Addr.Dynamic
+{
+	for(i := 0; i < len pool; i++)
+		if(pool[i].ip.eq(ipa))
+			return pool[i];
+	return nil;
+}
+
+poolfind(hwaddr, clientid: array of byte): ref Addr.Dynamic
+{
+	for(i := 0; i < len pool; i++)
+		if(leasematch(pool[i], hwaddr, clientid))
+			return pool[i];
+	return nil;
+}
+
+poolnextfree(): ref Addr.Dynamic
+{
+	# we walk through the pool with pooloff.  if the next is leased,
+	# try again from the start, but not before sorting longest unused to the front.
+	now := daytime->now();
+	if(leased(now, pool[pooloff])) {
+		pooloff = 0;
+		sort(pool, addrge);
+	}
+	if(leased(now, pool[pooloff]))
+		return nil;
+	return pool[pooloff++];
+}
+
+leased(now: int, a: ref Addr.Dynamic): int
+{
+	return a.leasetime != 0 && now <= a.leasestart+a.leasetime;
+}
+
+getlease(ipa: ref IPaddr, hwaddr, clientid: array of byte, otherok: int): ref Addr
+{
+	# check for fixed address
+	hwip := etherip(hwaddr);
+	if(hwip != nil)
+		return ref Addr.Fixed (*hwip);
+
+	# look for current/stale lease for this hwaddr/clientid first
+	if(otherok) {
+		a := poolfind(hwaddr, clientid);
+		if(a != nil)
+			return a;
+	}
+
+	# attempt to return the requested ip address
+	if(ipa != nil) {
+		now := daytime->now();
+		a := poolfindip(*ipa);
+		if(a != nil && (leasematch(a, hwaddr, clientid) || !leased(now, a)))
+			return a;
+		if(!otherok)
+			return nil;
+	}
+
+	# return any address
+	return poolnextfree();
+}
+
+clearlease(aa: ref Addr)
+{
+	pick a := aa {
+	Dynamic =>
+		a.leasetime = 0;
+		writestatefile(a);
+	}
+}
+
+putlease(aa: ref Addr, hwaddr, clientid: array of byte, leasetime: int, hostname: string)
+{
+	pick a := aa {
+	Dynamic =>
+		a.hwaddr = hwaddr;
+		a.clientid = clientid;
+		a.leasestart = daytime->now();
+		a.leasetime = leasetime;
+		a.hostname = hostname;
+		writestatefile(a);
+	}
+}
+
+findlease(ipa: IPaddr, hwaddr, clientid: array of byte): ref Addr
+{
+	match := etheripmatch(hwaddr, ipa);
+say(sprint("findlease, ip %s, hwaddr %s clientid %s, match %d (none,hit,bad)", ipa.text(), hex(hwaddr), hex(clientid), match));
+	case match {
+	Bad =>	return nil;
+	None =>	break;
+	Hit =>	return ref Addr.Fixed (ipa);
+	}
+
+	now := daytime->now();
+	for(i := 0; i < len pool; i++) {
+		a := pool[i];
+		if(!a.ip.eq(ipa))
+			continue;
+		if(!leased(now, a) || leasematch(a, hwaddr, clientid))
+			return a;
+		break;
+	}
+	return nil;
+}
+
+ipopt(code: int, s: string): ref Opt
+{
+	(ok, ipa) := IPaddr.parse(s);
+	if(ok < 0 || !ipa.isvalid() || !ipa.isv4()) {
+		warn(sprint("bad config, code %d, ip %q", code, s));
+		return nil;
+	}
+	return ref Opt (code, ipa.v4());
+}
+
+ipmaskopt(code: int, s: string): ref Opt
+{
+	(ok, ipa) := IPaddr.parsemask(s);
+	if(ok < 0) {
+		warn(sprint("bad config, code %d, ipmask %q", code, s));
+		return nil;
+	}
+	return ref Opt (code, ipa.v4());
+}
+
+intopt(code: int, v: int): ref Opt
+{
+	buf := array[4] of byte;
+	p32(buf, v);
+	return ref Opt (code, buf);
+}
+
+Config: adt {
+	opts:	list of ref Opt;
+	bootf:	string;
+	nextserver:	ref IPaddr;
+	leasetime:	int;
+};
+
+getconfig(ipaddr: string, req: array of byte, leasetimes: int): ref Config
+{
+	rattrs: list of string;
+	for(i := 0; i < len req; i++) {
+		r := int req[i];
+		if(r >= 0 && r < len optattrs && optattrs[r] != nil)
+			rattrs = optattrs[r]::rattrs;
+	}
+	leasetime0 := leasetime;
+	if(leasetimes)
+		rattrs = "leasetime"::rattrs;
+	rattrs = "ipmask"::"bootf"::"nextserver"::rattrs;
+	bootf: string;
+	nextserver: ref IPaddr;
+
+	# xxx for dns we should get all matches, not just a single dns server
+	opts: list of ref Opt;
+	(r, err) := ipval->findvals(ndb, ipaddr, rattrs);
+	if(err != nil)
+		return nil;
+	for(; r != nil; r = tl r) {
+		(k, v) := hd r;
+		if(v == nil)
+			continue;
+		o: ref Opt;
+		case k {
+		"sys" =>	o = ref Opt (dhcp->Ohostname, array of byte v);
+		"ipmask" =>	o = ipmaskopt(dhcp->Osubnetmask, v);
+		"ipgw" =>	o = ipopt(dhcp->Orouters, v);
+		"dns" =>	o = ipopt(dhcp->Odns, v);
+		"dnsdomain" =>	o = ref Opt (dhcp->Odomainname, array of byte v);
+		"bootf" =>	bootf = v;
+		"nextserver" =>
+				(ok, ipa) := IPaddr.parse(v);
+				if(ok >= 0 && ipa.isv4())
+					nextserver = ref ipa;
+		"tftp" =>	o = ref Opt (dhcp->Otftpservername, array of byte v);
+		"leasetime" =>	leasetime0 = int v;
+		}
+		if(o != nil)
+			opts = o::opts;
+	}
+	if(leasetimes) {
+		if(leasetime0 <= 1)
+			leasetime0 = leasetime;
+		opts = intopt(dhcp->Oleasetime, leasetime0)::opts;
+		opts = intopt(dhcp->Orenewaltime, 1*leasetime0/2)::opts;
+		opts = intopt(dhcp->Orebindingtime, 7*leasetime0/8)::opts;
+	}
+	if(bootf == nil)
+		nextserver = nil;
+	opts = ref Opt (dhcp->Oserverid, siaddr.v4())::opts;
+	return ref Config (opts, bootf, nextserver, leasetime0);
+}
+
+
+# client requests seeks new lease
+dhcpdiscover(im, om: ref Dhcpmsg)
+{
+	if(!im.ciaddr.eq(ip->v4noaddr))
+		return say("ignoring bad dhcp discover, ciaddr set");
+
+	clientid := getoption(im, dhcp->Oclientid);
+	reqip := getoptionip(im, dhcp->Oreqipaddr);
+	a := getlease(reqip, im.chaddr, clientid, 1);
+	if(a == nil)
+		return warn("no address available, ignoring dhcp discover");
+
+	paramreq := getoption(im, dhcp->Oparamreq);
+	c := getconfig(a.ip.text(), paramreq, 1);
+	om.file = c.bootf;
+	if(c.nextserver != nil)
+		om.siaddr = *c.nextserver;
+	om.opts = c.opts;
+
+	om.op = dhcp->Treply;
+	om.yiaddr = a.ip;
+
+	msgtype := ref Opt (dhcp->Odhcpmsgtype, array[] of {byte dhcp->TDoffer});
+	om.opts = msgtype::om.opts;
+}
+
+# client wants an offered lease, renew an existing lease, or refresh an old lease (eg after reboot)
+dhcprequest(im, om: ref Dhcpmsg)
+{
+	ipreq := getoptionip(im, dhcp->Oreqipaddr);
+	clientid := getoption(im, dhcp->Oclientid);
+	a: ref Addr;
+	if(im.ciaddr.eq(ip->v4noaddr)) {
+		if(ipreq == nil)
+			return say("ignoring bad dhcp request, missing 'requested ip address'");
+		a = getlease(ipreq, im.chaddr, clientid, 0);
+	} else {
+		serverid := getoption(im, dhcp->Oserverid);
+		if(serverid != nil)
+			return say("ignoring bad dhcp request, renew lease, but 'server identifier' present");
+		if(ipreq != nil)
+			say("ignoring bad dhcp request, 'requested ip address' present for renewing");
+		else
+			a = findlease(im.ciaddr, im.chaddr, clientid);
+	}
+	if(a == nil) {
+		om.op = dhcp->Treply;
+		msgtype := ref Opt (dhcp->Odhcpmsgtype, array[] of {byte dhcp->TDnak});
+		om.opts = msgtype::om.opts;
+		return;
+	}
+	paramreq := getoption(im, dhcp->Oparamreq);
+	c := getconfig(a.ip.text(), paramreq, 1);
+	om.file = c.bootf;
+	if(c.nextserver != nil)
+		om.siaddr = *c.nextserver;
+	om.opts = c.opts;
+
+	om.op = dhcp->Treply;
+	om.yiaddr = a.ip;
+	msgtype := ref Opt (dhcp->Odhcpmsgtype, array[] of {byte dhcp->TDack});
+	om.opts = msgtype::om.opts;
+
+	hostnamebuf := getoption(im, dhcp->Ohostname);
+	if(hostnamebuf != nil)
+		hostname := string hostnamebuf;
+	putlease(a, im.chaddr, clientid, c.leasetime, hostname);
+}
+
+# client rejects offered lease
+dhcpdecline(im, nil: ref Dhcpmsg)
+{
+	if(!im.ciaddr.eq(ip->v4noaddr))
+		return say("ignoring bad dhcp decline, ciaddr set");
+	if(!im.yiaddr.eq(ip->v4noaddr))
+		return say("ignoring bad dhcp decline, yiaddr set");
+
+	serverid := getoption(im, dhcp->Oserverid);
+	if(serverid == nil)
+		return say("ignoring bad dhcp decline, missing 'server identifier'");
+	reqip := getoptionip(im, dhcp->Oreqipaddr);
+	if(reqip == nil)
+		return say("ignoring bad dhcp decline, missing 'requested ip address'");
+	#clientid := getoption(im, dhcp->Oclientid);
+
+	say(sprint("client declined %s", (*reqip).text()));
+}
+
+# client gives back lease
+dhcprelease(im, nil: ref Dhcpmsg)
+{
+	if(im.ciaddr.eq(ip->v4noaddr))
+		return say("ignoring bad dhcp release, ciaddr not set");
+	serverid := getoption(im, dhcp->Oserverid);
+	if(serverid == nil)
+		return say("ignoring bad dhcp release, missing 'server identifier'");
+	if(getoption(im, dhcp->Oreqipaddr) != nil)
+		return say("ignoring bad dhcp release, bogus 'requested ip address' present");
+
+	clientid := getoption(im, dhcp->Oclientid);
+	a := findlease(im.ciaddr, im.chaddr, clientid);
+	if(a == nil)
+		return say("bogus dhcp release, ignoring");
+	clearlease(a);
+}
+
+# client has address configured (ciaddr), and just wants config parameters.
+# we respond with a dhcp ack similar to dhcprequest, but without:
+# - checking the address
+# - filling yiaddr
+# - sending lease time parameters
+dhcpinform(im, om: ref Dhcpmsg)
+{
+	if(im.ciaddr.eq(ip->v4noaddr))
+		return say("ignoring bad dhcp inform, ciaddr not set");
+	if(getoption(im, dhcp->Oreqipaddr) != nil)
+		return say("ignoring bad dhcp inform, bogus 'requested ip address' present");
+	if(getoption(im, dhcp->Oserverid) != nil)
+		return say("ignoring bad dhcp inform, bogus 'server identifier' present");
+	if(getoption(im, dhcp->Oleasetime) != nil)
+		return say("ignoring bad dhcp inform, bogus 'lease time' present");
+
+	paramreq := getoption(im, dhcp->Oparamreq);
+	c := getconfig(im.ciaddr.text(), paramreq, 0);
+	om.file = c.bootf;
+	om.opts = c.opts;
+	om.op = dhcp->Treply;
+	msgtype := ref Opt (dhcp->Odhcpmsgtype, array[] of {byte dhcp->TDack});
+	om.opts = msgtype::om.opts;
+	# xxx should unicast the response, not broadcast
+}
+
+dhcpsrv(td: int, im: ref Dhcpmsg): ref Dhcpmsg
+{
+	om := ref *im;
+	om.op = ~0;
+	om.ciaddr = om.yiaddr = om.giaddr = ip->v4noaddr;
+	om.siaddr = siaddr;
+	om.chaddr = im.chaddr;
+	om.sname = nil;
+	om.file = nil;
+	om.options = array[4] of byte;
+	p32(om.options, dhcp->Moptions);
+	om.opts = nil;
+	case td {
+	dhcp->TDdiscover =>	dhcpdiscover(im, om);
+	dhcp->TDrequest =>	dhcprequest(im, om);
+	dhcp->TDdecline =>	dhcpdecline(im, om);
+	dhcp->TDrelease =>	dhcprelease(im, om);
+	dhcp->TDinform =>	dhcpinform(im, om);
+	dhcp->TDoffer =>	say("bad, dhcp offer to server port");
+	dhcp->TDack =>		say("bad, dhcp ack to server port");
+	dhcp->TDnak =>		say("bad, dhcp nak to server port");
+	* =>			say(sprint("unrecognized dhcp message type %d", td));
+	}
+	if(om.op == ~0)
+		return nil;
+	om.opts = lists->reverse(ref Opt (dhcp->Oend, nil)::om.opts);
+	return om;
+}
+
+readfile(name: string): string
+{
+	fd := sys->open(name, Sys->OREAD);
+	buf := array[256] of byte;
+	n := sys->read(fd, buf, len buf);
+	if(n <= 0)
+		return nil;
+	return string buf[0:n];
+}
+
+writefile(name, s: string): string
+{
+	buf := array of byte s;
+	fd := sys->create(name, Sys->OWRITE|Sys->OTRUNC, 8r666);
+	if(fd == nil || sys->write(fd, buf, len buf) != len buf)
+		return sprint("%r");
+	return nil;
+}
+
+
+eq(a, b: array of byte): int
+{
+	if(len a != len b)
+		return 0;
+	for(i := 0; i < len a; i++)
+		if(a[i] != b[i])
+			return 0;
+	return 1;
+}
+
+ipcmp(ipa, ipb: IPaddr): int
+{
+	a := ipa.v6();
+	b := ipb.v6();
+	for(i := 0; i < len a; i++)
+		if(a[i] != b[i])
+			return int a[i]-int b[i];
+	return 0;
+}
+
+g32(d: array of byte): int
+{
+	v := 0;
+	v |= int d[0]<<24;
+	v |= int d[1]<<16;
+	v |= int d[2]<<8;
+	v |= int d[3]<<0;
+	return v;
+}
+
+g16(d: array of byte): int
+{
+	v := 0;
+	v |= int d[2]<<8;
+	v |= int d[3]<<0;
+	return v;
+}
+
+p32(d: array of byte, v: int): int
+{
+	d[0] = byte (v>>24);
+	d[1] = byte (v>>16);
+	d[2] = byte (v>>8);
+	d[3] = byte (v>>0);
+	return 4;
+}
+
+p16(d: array of byte, v: int): int
+{
+	d[0] = byte (v>>8);
+	d[1] = byte (v>>0);
+	return 2;
+}
+
+
+hex(d: array of byte): string
+{
+	if(d == nil)
+		return "";
+	return base16->enc(d);
+}
+
+sort[T](a: array of T, ge: ref fn(a, b: T): int)
+{
+	for(i := 1; i < len a; i++) {
+		tmp := a[i];
+		for(j := i; j > 0 && ge(a[j-1], tmp); j--)
+			a[j] = a[j-1];
+		a[j] = tmp;
+	}
+}
+
+l2a[T](l: list of T): array of T
+{
+	a := array[len l] of T;
+	i := 0;
+	for(; l != nil; l = tl l)
+		a[i++] = hd l;
+	return a;
+}
+
+say(s: string)
+{
+	if(dflag)
+		warn(s);
+}
+
+warn(s: string)
+{
+	sys->fprint(sys->fildes(2), "%s\n", s);
+}
+
+fail(s: string)
+{
+	warn(s);
+	raise "fail:"+s;
+}
--- a/appl/cmd/ip/mkfile	Fri Aug 13 11:37:31 2021
+++ b/appl/cmd/ip/mkfile	Fri Aug 13 08:24:53 2021
@@ -7,6 +7,7 @@
 TARG=\
 	bootpd.dis\
 	dhcp.dis\
+	dhcpd.dis\
 	ping.dis\
 	rip.dis\
 	tftpd.dis\
@@ -14,13 +15,18 @@
 	sntp.dis\
 
 SYSMODULES=\
+	arg.m\
 	attrdb.m\
 	bufio.m\
+	daytime.m\
 	dhcp.m\
 	draw.m\
+	encoding.m\
 	ether.m\
 	ip.m\
 	ipattr.m\
+	lists.m\
+	string.m\
 	sys.m\
 
 DISBIN=$ROOT/dis/ip
--- /dev/null	Mon Oct 18 12:05:45 2021
+++ b/appl/lib/dhcp.b	Fri Aug 13 08:24:53 2021
@@ -0,0 +1,1033 @@
+implement Dhcpclient;
+
+#
+# DHCP and BOOTP clients
+# Copyright © 2004-2006 Vita Nuova Holdings Limited
+#
+
+include "sys.m";
+	sys: Sys;
+
+include "ip.m";
+	ip: IP;
+	IPv4off, IPaddrlen, Udphdrlen, Udpraddr, Udpladdr, Udprport, Udplport: import IP;
+	IPaddr: import ip;
+	get2, get4, put2, put4: import ip;
+
+include "keyring.m";
+include "security.m";	# for Random
+
+include "dial.m";
+	dial: Dial;
+
+include "dhcp.m";
+
+debug := 0;
+
+xidgen: int;
+
+init()
+{
+	sys = load Sys Sys->PATH;
+	random := load Random Random->PATH;
+	if(random != nil)
+		xidgen = random->randomint(Random->NotQuiteRandom);
+	else
+		xidgen = sys->pctl(0, nil)*sys->millisec();
+	random = nil;
+	dial = load Dial Dial->PATH;
+	ip = load IP IP->PATH;
+	ip->init();
+}
+
+tracing(d: int)
+{
+	debug = d;
+}
+
+Bootconf.new(): ref Bootconf
+{
+	bc := ref Bootconf;
+	bc.lease = 0;
+	bc.options = array[256] of array of byte;
+	return bc;
+}
+
+Bootconf.get(c: self ref Bootconf, n: int): array of byte
+{
+	a := c.options;
+	if(n & Ovendor){
+		a = c.vendor;
+		n &= ~Ovendor;
+	}
+	if(n < 0 || n >= len a)
+		return nil;
+	return a[n];
+}
+
+Bootconf.getint(c: self ref Bootconf, n: int): int
+{
+	a := c.get(n);
+	v := 0;
+	for(i := 0; i < len a; i++)
+		v = (v<<8) | int a[i];
+	return v;
+}
+
+Bootconf.getip(c: self ref Bootconf, n: int): string
+{
+	l := c.getips(n);
+	if(l == nil)
+		return nil;
+	return hd l;
+}
+
+Bootconf.getips(c: self ref Bootconf, n: int): list of string
+{
+	a := c.get(n);
+	rl: list of string;
+	while(len a >= 4){
+		rl = v4text(a) :: rl;
+		a = a[4:];
+	}
+	l: list of string;
+	for(; rl != nil; rl = tl rl)
+		l = hd rl :: l;
+	return l;
+}
+
+Bootconf.gets(c: self ref Bootconf, n: int): string
+{
+	a := c.get(n);
+	if(a == nil)
+		return nil;
+	for(i:=0; i<len a; i++)
+		if(a[i] == byte 0)
+			break;
+	return string a[0:i];
+}
+
+Bootconf.put(c: self ref Bootconf, n: int, a: array of byte)
+{
+	if(n < 0 || n >= len c.options)
+		return;
+	ca := array[len a] of byte;
+	ca[0:] = a;
+	c.options[n] = ca;
+}
+
+Bootconf.putint(c: self ref Bootconf, n: int, v: int)
+{
+	if(n < 0 || n >= len c.options)
+		return;
+	a := array[4] of byte;
+	put4(a, 0, v);
+	c.options[n] = a;
+}
+
+Bootconf.putips(c: self ref Bootconf, n: int, ips: list of string)
+{
+	if(n < 0 || n >= len c.options)
+		return;
+	na := len ips;
+	a := array[na*4] of byte;
+	na = 0;
+	for(; ips != nil; ips = tl ips){
+		(nil, ipa) := IPaddr.parse(hd ips);
+		a[na++:] = ipa.v4();
+	}
+	c.options[n] = a;
+}
+
+Bootconf.puts(c: self ref Bootconf, n: int, s: string)
+{
+	if(n < 0 || n >= len c.options)
+		return;
+	c.options[n] = array of byte s;
+}
+
+#
+#
+# DHCP
+#
+#
+
+# BOOTP operations
+Bootprequest, Bootpreply: con 1+iota;
+
+# DHCP operations
+NotDHCP, Discover, Offer, Request, Decline, Ack, Nak, Release, Inform: con iota;
+
+Dhcp: adt {
+	udphdr:	array of byte;
+	op:		int;
+	htype:	int;
+	hops:	int;
+	xid:		int;
+	secs:		int;
+	flags:	int;
+	ciaddr:	IPaddr;
+	yiaddr:	IPaddr;
+	siaddr:	IPaddr;
+	giaddr:	IPaddr;
+	chaddr:	array of byte;
+	sname:	string;
+	file:		string;
+	options:	list of (int, array of byte);
+	dhcpop:	int;
+};
+
+opnames := array[] of {
+	Discover => "Discover",
+	Offer => "Offer",
+	Request => "Request",
+	Decline => "Decline",
+	Ack => "Ack",
+	Nak => "Nak",
+	Release => "Release",
+	Inform => "Inform"
+};
+
+opname(op: int): string
+{
+	if(op >= 0 && op < len opnames)
+		return opnames[op];
+	return sys->sprint("OP%d", op);
+}
+
+stringget(buf: array of byte): string
+{
+	for(x := 0; x < len buf; x++)
+		if(buf[x] == byte 0)
+			break;
+	if(x == 0)
+		return nil;
+	return string buf[0 : x];
+}
+
+eqbytes(b1: array of byte, b2: array of byte): int
+{
+	l := len b1;
+	if(l != len b2)
+		return 0;
+	for(i := 0; i < l; i++)
+		if(b1[i] != b2[i])
+			return 0;
+	return 1;
+}
+
+magic := array[] of {byte 99, byte 130, byte 83, byte 99};	# RFC2132 (replacing RFC1048)
+
+dhcpsend(fd: ref Sys->FD, xid: int, dhcp: ref Dhcp)
+{
+	dhcp.xid = xid;
+	abuf := array[576+Udphdrlen] of {* => byte 0};
+	abuf[0:] = dhcp.udphdr;
+	buf := abuf[Udphdrlen:];
+	buf[0] = byte dhcp.op;
+	buf[1] = byte dhcp.htype;
+	buf[2] = byte len dhcp.chaddr;
+	buf[3] = byte dhcp.hops;
+	put4(buf, 4, xid);
+	put2(buf, 8, dhcp.secs);
+	put2(buf, 10, dhcp.flags);
+	buf[12:] = dhcp.ciaddr.v4();
+	buf[16:] = dhcp.yiaddr.v4();
+	buf[20:] = dhcp.siaddr.v4();
+	buf[24:] = dhcp.giaddr.v4();
+	buf[28:] = dhcp.chaddr;
+	buf[44:] = array of byte dhcp.sname;	# [64]
+	buf[108:] = array of byte dhcp.file;	# [128]
+	o := 236;
+	# RFC1542 suggests including magic and Oend as a minimum, even in BOOTP
+	buf[o:] = magic;
+	o += 4;
+	if(dhcp.dhcpop != NotDHCP){
+		buf[o++] = byte Otype;
+		buf[o++] = byte 1;
+		buf[o++] = byte dhcp.dhcpop;
+	}
+	for(ol := dhcp.options; ol != nil; ol = tl ol){
+		(opt, val) := hd ol;
+		buf[o++] = byte opt;
+		buf[o++] = byte len val;
+		if(len val > 0){
+			buf[o:] = val;
+			o += len val;
+		}
+	}
+	buf[o++] = byte Oend;
+	if(debug)
+		dumpdhcp(dhcp, "->");
+	sys->write(fd, abuf, len abuf);
+}
+
+kill(pid: int, grp: string)
+{
+	fd := sys->open("#p/" + string pid + "/ctl", sys->OWRITE);
+	if(fd != nil)
+		sys->fprint(fd, "kill%s", grp);
+}
+
+v4text(a: array of byte): string
+{
+	return sys->sprint("%ud.%ud.%ud.%ud", int a[0], int a[1], int a[2], int a[3]);
+}
+
+parseopt(a: array of byte, isdhcp: int): (int, list of (int, array of byte))
+{
+	opts: list of (int, array of byte);
+	xop := NotDHCP;
+	for(i := 0; i < len a;){
+		op := int a[i++];
+		if(op == Opad)
+			continue;
+		if(op == Oend || i >= len a)
+			break;
+		l := int a[i++];
+		if(i+l > len a)
+			break;
+		if(isdhcp && op == Otype)
+			xop = int a[i];
+		else
+			opts = (op, a[i:i+l]) :: opts;
+		i += l;
+	}
+	rl := opts;
+	opts = nil;
+	for(; rl != nil; rl = tl rl)
+		opts = hd rl :: opts;
+	return (xop, opts);
+}
+
+dhcpreader(pidc: chan of int, srv: ref DhcpIO)
+{
+	pidc <-= sys->pctl(0, nil);
+	for(;;){
+		abuf := array [576+Udphdrlen] of byte;
+		n := sys->read(srv.fd, abuf, len abuf);
+		if(n < 0){
+			if(debug)
+				sys->print("read error: %r\n");
+			sys->sleep(1000);
+			continue;
+		}
+		if(n < Udphdrlen+236){
+			if(debug)
+				sys->print("short read: %d\n", n);
+			continue;
+		}
+		buf := abuf[Udphdrlen:n];
+		n -= Udphdrlen;
+		dhcp := ref Dhcp;
+		dhcp.op = int buf[0];
+		if(dhcp.op != Bootpreply){
+			if(debug)
+				sys->print("bootp: not reply, discarded\n");
+			continue;
+		}
+		dhcp.dhcpop = NotDHCP;
+		if(n >= 240 && eqbytes(buf[236:240], magic))	# otherwise it's something we won't understand
+			(dhcp.dhcpop, dhcp.options) = parseopt(buf[240:n], 1);
+		case dhcp.dhcpop {
+		NotDHCP or Ack or Nak or Offer =>
+			;
+		* =>
+			if(debug)
+				sys->print("dhcp: ignore dhcp op %d\n", dhcp.dhcpop);
+			continue;
+		}
+		dhcp.udphdr = abuf[0:Udphdrlen];
+		dhcp.htype = int buf[1];
+		hlen := int buf[2];
+		dhcp.hops = int buf[3];
+		dhcp.xid = get4(buf, 4);
+		dhcp.secs = get2(buf, 8);
+		dhcp.flags = get2(buf, 10);
+		dhcp.ciaddr = IPaddr.newv4(buf[12:]);
+		dhcp.yiaddr = IPaddr.newv4(buf[16:]);
+		dhcp.siaddr = IPaddr.newv4(buf[20:]);
+		dhcp.giaddr = IPaddr.newv4(buf[24:]);
+		dhcp.chaddr = buf[28 : 28 + hlen];
+		dhcp.sname = stringget(buf[44 : 108]);
+		dhcp.file = stringget(buf[108 : 236]);
+		srv.dc <-= dhcp;
+	}
+}
+
+timeoutstart(msecs: int): (int, chan of int)
+{
+	tc := chan of int;
+	spawn timeoutproc(tc, msecs);
+	return (<-tc, tc);
+}
+
+timeoutproc(c: chan of int, msecs: int)
+{
+	c <-= sys->pctl(0, nil);
+	sys->sleep(msecs);
+	c <-= 1;
+}
+
+hex(b: int): int
+{
+	if(b >= '0' && b <= '9')
+		return b-'0';
+	if(b >= 'A' && b <= 'F')
+		return b-'A' + 10;
+	if(b >= 'a' && b <= 'f')
+		return b-'a' + 10;
+	return -1;
+}
+
+gethaddr(device: string): (int, string, array of byte)
+{
+	fd := sys->open(device, Sys->OREAD);
+	if(fd == nil)
+		return (-1, sys->sprint("%r"), nil);
+	buf := array [100] of byte;
+	n := sys->read(fd, buf, len buf);
+	if(n < 0)
+		return (-1, sys->sprint("%r"), nil);
+	if(n == 0)
+		return (-1, "empty address file", nil);
+	addr := array [n/2] of byte;
+	for(i := 0; i < len addr; i++){
+		u := hex(int buf[2*i]);
+		l := hex(int buf[2*i+1]);
+		if(u < 0 || l < 0)
+			return (-1, "bad address syntax", nil);
+		addr[i] = byte ((u<<4)|l);
+	}
+	return (1, nil, addr);
+}
+
+newrequest(dest: IPaddr, bootfile: string, htype: int, haddr: array of byte, ipaddr: IPaddr, options: array of array of byte): ref Dhcp
+{
+	dhcp := ref Dhcp;
+	dhcp.op = Bootprequest;
+	hdr := array[Udphdrlen] of {* => byte 0};
+	hdr[Udpraddr:] = dest.v6();
+	put2(hdr, Udprport, 67);
+	dhcp.udphdr = hdr;
+	dhcp.htype = htype;
+	dhcp.chaddr = haddr;
+	dhcp.hops = 0;
+	dhcp.secs = 0;
+	dhcp.flags = 0;
+	dhcp.xid = 0;
+	dhcp.ciaddr = ipaddr;
+	dhcp.yiaddr = ip->v4noaddr;
+	dhcp.siaddr = ip->v4noaddr;
+	dhcp.giaddr = ip->v4noaddr;
+	dhcp.file = bootfile;
+	dhcp.dhcpop = NotDHCP;
+	if(options != nil){
+		for(i := 0; i < len options; i++)
+			if(options[i] != nil)
+				dhcp.options = (i, options[i]) :: dhcp.options;
+	}
+	clientid := array[len haddr + 1] of byte;
+	clientid[0] = byte htype;
+	clientid[1:] = haddr;
+	dhcp.options = (Oclientid, clientid) :: dhcp.options;
+	dhcp.options = (Ovendorclass, array of byte "plan9_386") :: dhcp.options;	# 386 will do because type doesn't matter
+	return dhcp;
+}
+
+udpannounce(net: string): (ref Sys->FD, string)
+{
+	if(net == nil)
+		net = "/net";
+	conn := dial->announce(net+"/udp!*!68");
+	if(conn == nil)
+		return (nil, sys->sprint("can't announce dhcp port: %r"));
+	if(sys->fprint(conn.cfd, "headers") < 0)
+		return (nil, sys->sprint("can't set headers mode on dhcp port: %r"));
+	conn.dfd = sys->open(conn.dir+"/data", Sys->ORDWR);
+	if(conn.dfd == nil)
+		return (nil, sys->sprint("can't open %s: %r", conn.dir+"/data"));
+	return (conn.dfd, nil);
+}
+
+ifcnoaddr(fd: ref Sys->FD, s: string)
+{
+	if(fd != nil && sys->fprint(fd, "%s %s %s", s, (ip->noaddr).text(), (ip->noaddr).text()) < 0){
+		if(debug)
+			sys->print("dhcp: ctl %s: %r\n", s);
+	}
+}
+
+setup(net: string, device: string, init: ref Bootconf): (ref Dhcp, ref DhcpIO, string)
+{
+	(htype, err, mac) := gethaddr(device);
+	if(htype < 0)
+		return (nil, nil, sys->sprint("can't get hardware MAC address: %s", err));
+	ciaddr := ip->v4noaddr;
+	if(init != nil && init.ip != nil){
+		valid: int;
+		(valid, ciaddr) = IPaddr.parse(init.ip);
+		if(valid < 0)
+			return (nil, nil, sys->sprint("invalid ip address: %s", init.ip));
+	}
+	(dfd, err2) := udpannounce(net);
+	if(err2 != nil)
+		return (nil, nil, err);
+	bootfile: string;
+	options: array of array of byte;
+	if(init != nil){
+		bootfile = init.bootf;
+		options = init.options;
+	}
+	return (newrequest(ip->v4bcast, bootfile, htype, mac, ciaddr, options), DhcpIO.new(dfd), nil);
+}
+
+#
+# BOOTP (RFC951) is used by Inferno only during net boots, to get initial IP address and TFTP address and parameters
+#
+bootp(net: string, ctlifc: ref Sys->FD, device: string, init: ref Bootconf): (ref Bootconf, string)
+{
+	(req, srv, err) := setup(net, device, init);
+	if(err != nil)
+		return (nil, err);
+	ifcnoaddr(ctlifc, "add");
+	rdhcp := exchange(srv, ++xidgen, req, 1<<NotDHCP);
+	srv.rstop();
+	ifcnoaddr(ctlifc, "remove");
+	if(rdhcp == nil)
+		return (nil, "no response to BOOTP request");
+	return (fillbootconf(init, rdhcp), nil);
+}
+
+defparams := array[] of {
+	byte Omask, byte Orouter, byte Odnsserver, byte Ohostname, byte Odomainname, byte Ontpserver,
+};
+
+#
+# DHCP (RFC2131)
+#
+dhcp(net: string, ctlifc: ref Sys->FD, device: string, init: ref Bootconf, needparam: array of int): (ref Bootconf, ref Lease, string)
+{
+	(req, srv, err) := setup(net, device, init);
+	if(err != nil)
+		return (nil, nil, err);
+	params := defparams;
+	if(needparam != nil){
+		n := len defparams;
+		params = array[n+len needparam] of byte;
+		params[0:] = defparams;
+		for(i := 0; i < len needparam; i++)
+			params[n+i] = byte needparam[i];
+	}
+	initopt := (Oparams, params) :: req.options;	# RFC2131 requires parameters to be repeated each time
+	lease := ref Lease(0, chan[1] of (ref Bootconf, string));
+	spawn dhcp1(srv, lease, net, ctlifc, req, init, initopt);
+	bc: ref Bootconf;
+	(bc, err) = <-lease.configs;
+	return (bc, lease, err);
+}
+
+dhcp1(srv: ref DhcpIO, lease: ref Lease, net: string, ctlifc: ref Sys->FD, req: ref Dhcp, init: ref Bootconf, initopt: list of (int, array of byte))
+{
+	cfd := -1;
+	if(ctlifc != nil)
+		cfd = ctlifc.fd;
+	lease.pid = sys->pctl(Sys->NEWPGRP|Sys->NEWFD, 1 :: srv.fd.fd :: cfd :: nil);
+	if(ctlifc != nil)
+		ctlifc = sys->fildes(ctlifc.fd);
+	srv.fd = sys->fildes(srv.fd.fd);
+	rep: ref Dhcp;
+	ifcnoaddr(ctlifc, "add");
+	if(req.ciaddr.isvalid())
+		rep = reacquire(srv, req, initopt, req.ciaddr);
+	if(rep == nil)
+		rep = askround(srv, req, initopt);
+	srv.rstop();
+	ifcnoaddr(ctlifc, "remove");
+	if(rep == nil){
+		lease.pid = 0;
+		lease.configs <-= (nil, "no response");
+		exit;
+	}
+	for(;;){
+		conf := fillbootconf(init, rep);
+		applycfg(net, ctlifc, conf);
+		if(conf.lease == 0){
+			srv.rstop();
+			lease.pid = 0;
+			flush(lease.configs);
+			lease.configs <-= (conf, nil);
+			exit;
+		}
+		flush(lease.configs);
+		lease.configs <-= (conf, nil);
+		req.ciaddr = rep.yiaddr;
+		while((rep = tenancy(srv, req, conf.lease)) != nil){
+			if(rep.dhcpop == Nak || !rep.ciaddr.eq(req.ciaddr))
+				break;
+			req.udphdr[Udpraddr:] = rep.udphdr[Udpraddr:Udpraddr+IPaddrlen];
+			conf = fillbootconf(init, rep);
+		}
+		removecfg(net, ctlifc, conf);
+		ifcnoaddr(ctlifc, "add");
+		while((rep = askround(srv, req, initopt)) == nil){
+			flush(lease.configs);
+			lease.configs <-= (nil, "no response");
+			srv.rstop();
+			sys->sleep(60*1000);
+		}
+		ifcnoaddr(ctlifc, "remove");
+	}
+}
+
+reacquire(srv: ref DhcpIO, req: ref Dhcp, initopt: list of (int, array of byte), addr: IPaddr): ref Dhcp
+{
+	# INIT-REBOOT: know an address; try requesting it (once)
+	# TO DO: could use Inform when our address is static but we need a few service parameters
+	req.ciaddr = ip->v4noaddr;
+	rep := request(srv, ++xidgen, req, (Oipaddr, addr.v4()) :: initopt);
+	if(rep != nil && rep.dhcpop == Ack && addr.eq(rep.yiaddr)){
+		if(debug)
+			sys->print("req: server accepted\n");
+		req.udphdr[Udpraddr:] = rep.udphdr[Udpraddr:Udpraddr+IPaddrlen];
+		return rep;
+	}
+	if(debug)
+		sys->print("req: cannot reclaim\n");
+	return nil;
+}
+
+askround(srv: ref DhcpIO, req: ref Dhcp, initopt: list of (int, array of byte)): ref Dhcp
+{
+	# INIT
+	req.ciaddr = ip->v4noaddr;
+	req.udphdr[Udpraddr:] = (ip->v4bcast).v6();
+	for(retries := 0; retries < 5; retries++){
+		# SELECTING
+		req.dhcpop = Discover;
+		req.options = initopt;
+		rep := exchange(srv, ++xidgen, req, 1<<Offer);
+		if(rep == nil)
+			break;
+		#
+		# could wait a little while and accumulate offers, but is it sensible?
+		# we do sometimes see arguments between DHCP servers that could
+		# only be resolved by user choice
+		#
+		if(!rep.yiaddr.isvalid())
+			continue;		# server has no idea either
+		serverid := getopt(rep.options, Oserverid, 4);
+		if(serverid == nil)
+			continue;	# broken server
+		# REQUESTING
+		options := (Oserverid, serverid) :: (Oipaddr, rep.yiaddr.v4()) :: initopt;
+		lease := getlease(rep);
+		if(lease != nil)
+			options = (Olease, lease) :: options;
+		rep = request(srv, rep.xid, req, options);
+		if(rep != nil){
+			# could probe with ARP here, and if found, Decline
+			if(debug)
+				sys->print("req: server accepted\n");
+			req.udphdr[Udpraddr:] = rep.udphdr[Udpraddr:Udpraddr+IPaddrlen];
+			return rep;
+		}
+	}
+	return nil;
+}
+
+request(srv: ref DhcpIO, xid: int, req: ref Dhcp, options: list of (int, array of byte)): ref Dhcp
+{
+	req.dhcpop = Request;	# Selecting
+	req.options = options;
+	rep := exchange(srv, xid, req, (1<<Ack)|(1<<Nak));
+	if(rep == nil || rep.dhcpop == Nak)
+		return nil;
+	return rep;
+}
+
+# renew
+#	direct to server from T1 to T2 [RENEW]
+#	Request must not include
+#		requested IP address, server identifier
+#	Request must include
+#		ciaddr set to client's address
+#	Request might include
+#		lease time
+#	similar, but broadcast, from T2 to T3 [REBIND]
+#	at T3, unbind, restart Discover
+
+tenancy(srv: ref DhcpIO, req: ref Dhcp, leasesec: int): ref Dhcp
+{
+	# configure address...
+	t3 := big leasesec * big 1000;	# lease expires; restart
+	t2 := (big 3 * t3)/big 4;	# broadcast renewal request at ¾time
+	t1 := t2/big 2;		# renew lease with original server at ½time
+	srv.rstop();
+	thebigsleep(t1);
+	# RENEW
+	rep := renewing(srv, req, t1, t2);
+	if(rep != nil)
+		return rep;
+	# REBIND
+	req.udphdr[Udpraddr:] = (ip->v4bcast).v6();	# now try broadcast
+	return renewing(srv, req, t2, t3);
+}
+
+renewing(srv: ref DhcpIO, req: ref Dhcp, a: big, b: big): ref Dhcp
+{
+	Minute: con big(60*1000);
+	while(a < b){
+		rep := exchange(srv, req.xid, req, (1<<Ack)|(1<<Nak));
+		if(rep != nil)
+			return rep;
+		delta := (b-a)/big 2;
+		if(delta < Minute)
+			delta = Minute;
+		thebigsleep(delta);
+		a += delta;
+	}
+	return nil;
+}
+
+thebigsleep(msec: big)
+{
+	Day: con big (24*3600*1000);	# 1 day in msec
+	while(msec > big 0){
+		n := msec;
+		if(n > Day)
+			n = Day;
+		sys->sleep(int n);
+		msec -= n;
+	}
+}
+
+getlease(m: ref Dhcp): array of byte
+{
+	lease := getopt(m.options, Olease, 4);
+	if(lease == nil)
+		return nil;
+	if(get4(lease, 0) == 0){
+		lease = array[4] of byte;
+		put4(lease, 0, 15*60);
+	}
+	return lease;
+}
+
+fillbootconf(init: ref Bootconf, pkt: ref Dhcp): ref Bootconf
+{
+	bc := ref Bootconf;
+	if(init != nil)
+		*bc = *init;
+	if(bc.options == nil)
+		bc.options = array[256] of array of byte;
+	for(l := pkt.options; l != nil; l = tl l){
+		(c, v) := hd l;
+		if(bc.options[c] == nil)
+			bc.options[c] = v;	# give priority to first occurring
+	}
+	if((a := bc.get(Ovendorinfo)) != nil){
+		if(bc.vendor == nil)
+			bc.vendor = array[256] of array of byte;
+		for(l = parseopt(a, 0).t1; l  != nil; l = tl l){
+			(c, v) := hd l;
+			if(bc.vendor[c] == nil)
+				bc.vendor[c] = v;
+		}
+	}
+	if(pkt.yiaddr.isvalid()){
+		bc.ip = pkt.yiaddr.text();
+		bc.ipmask = bc.getip(Omask);
+		if(bc.ipmask == nil)
+			bc.ipmask = pkt.yiaddr.classmask().masktext();
+	}
+	bc.bootf = pkt.file;
+	bc.dhcpip = IPaddr.newv6(pkt.udphdr[Udpraddr:]).text();
+	bc.siaddr = pkt.siaddr.text();
+	bc.lease = bc.getint(Olease);
+	if(bc.lease == Infinite)
+		bc.lease = 0;
+	else if(debug > 1)
+		bc.lease = 2*60;	# shorten time, for testing
+	bc.dom = bc.gets(Odomainname);
+	s := bc.gets(Ohostname);
+	for(i:=0; i<len s; i++)
+		if(s[i] == '.'){
+			if(bc.dom == nil)
+				bc.dom = s[i+1:];
+			s = s[0:i];
+			break;
+		}
+	bc.sys = s;
+	bc.ipgw = bc.getip(Orouter);
+	bc.bootip = bc.getip(Otftpserver);
+	bc.serverid = bc.getip(Oserverid);
+	return bc;
+}
+
+Lease.release(l: self ref Lease)
+{
+	# could send a Release message
+	# should unconfigure
+	if(l.pid){
+		kill(l.pid, "grp");
+		l.pid = 0;
+	}
+}
+
+flush(c: chan of (ref Bootconf, string))
+{
+	alt{
+	<-c =>	;
+	* =>	;
+	}
+}
+
+DhcpIO: adt {
+	fd:	ref Sys->FD;
+	pid:	int;
+	dc:	chan of ref Dhcp;
+	new:	fn(fd: ref Sys->FD): ref DhcpIO;
+	rstart:	fn(io: self ref DhcpIO);
+	rstop:	fn(io: self ref DhcpIO);
+};
+
+DhcpIO.new(fd: ref Sys->FD): ref DhcpIO
+{
+	return ref DhcpIO(fd, 0, chan of ref Dhcp);
+}
+
+DhcpIO.rstart(io: self ref DhcpIO)
+{
+	if(io.pid == 0){
+		pids := chan of int;
+		spawn dhcpreader(pids, io);
+		io.pid = <-pids;
+	}
+}
+
+DhcpIO.rstop(io: self ref DhcpIO)
+{
+	if(io.pid != 0){
+		kill(io.pid, "");
+		io.pid = 0;
+	}
+}
+
+getopt(options: list of (int, array of byte), op: int, minlen: int): array of byte
+{
+	for(; options != nil; options = tl options){
+		(opt, val) := hd options;
+		if(opt == op && len val >= minlen)
+			return val;
+	}
+	return nil;
+}
+
+exchange(srv: ref DhcpIO, xid: int, req: ref Dhcp, accept: int): ref Dhcp
+{
+	srv.rstart();
+	nsec := 3;
+	for(count := 0; count < 5; count++) {
+		(tpid, tc) := timeoutstart(nsec*1000);
+		dhcpsend(srv.fd, xid, req);
+	   Wait:
+		for(;;){
+			alt {
+			<-tc=>
+				break Wait;
+			rep := <-srv.dc=>
+				if(debug)
+					dumpdhcp(rep, "<-");
+				if(rep.op == Bootpreply &&
+				    rep.xid == req.xid &&
+				    rep.ciaddr.eq(req.ciaddr) &&
+				    eqbytes(rep.chaddr, req.chaddr)){
+					if((accept & (1<<rep.dhcpop)) == 0){
+						if(debug)
+							sys->print("req: unexpected reply %s to %s\n", opname(rep.dhcpop), opname(req.dhcpop));
+						continue;
+					}
+					kill(tpid, "");
+					return rep;
+				}
+				if(debug)
+					sys->print("req: mismatch\n");
+			}
+		}
+		req.secs += nsec;
+		nsec++;
+	}
+	return nil;
+}
+
+applycfg(net: string, ctlfd: ref Sys->FD, bc: ref Bootconf): string
+{
+	# write addresses to /net/...
+	# local address, mask[or default], remote address [mtu]
+	if(net == nil)
+		net = "/net";
+	if(bc.ip == nil)
+		return  "invalid address";
+	if(ctlfd != nil){
+		if(sys->fprint(ctlfd, "add %s %s", bc.ip, bc.ipmask) < 0)	# TO DO: [raddr [mtu]]
+			return sys->sprint("add interface: %r");
+		# could use "mtu n" request to set/change mtu
+	}
+	# if primary:
+	# 	add default route if gateway valid
+	# 	put ndb entries ip=, ipmask=, ipgw=; sys= dom=; fs=; auth=; dns=; ntp=; other options from bc.options
+	if(bc.ipgw != nil){
+		fd := sys->open(net+"/iproute", Sys->OWRITE);
+		if(fd != nil)
+			sys->fprint(fd, "add 0 0 %s", bc.ipgw);
+	}
+	s := sys->sprint("ip=%s ipmask=%s", bc.ip, bc.ipmask);
+	if(bc.ipgw != nil)
+		s += sys->sprint(" ipgw=%s", bc.ipgw);
+	s += "\n";
+	if(bc.sys != nil)
+		s += sys->sprint("	sys=%s\n", bc.sys);
+	if(bc.dom != nil)
+		s += sys->sprint("	dom=%s.%s\n", bc.sys, bc.dom);
+	if((addr := bc.getip(OP9auth)) != nil)
+		s += sys->sprint("	auth=%s\n", addr);	# TO DO: several addresses
+	if((addr = bc.getip(OP9fs)) != nil)
+		s += sys->sprint("	fs=%s\n", addr);
+	if((addr = bc.getip(Odnsserver)) != nil)
+		s += sys->sprint("	dns=%s\n", addr);
+	fd := sys->open(net+"/ndb", Sys->OWRITE | Sys->OTRUNC);
+	if(fd != nil){
+		a := array of byte s;
+		sys->write(fd, a, len a);
+	}
+	return nil;
+}
+
+removecfg(nil: string, ctlfd: ref Sys->FD, bc: ref Bootconf): string
+{
+	# remove localaddr, localmask[or default]
+	if(ctlfd != nil){
+		if(sys->fprint(ctlfd, "remove %s %s", bc.ip, bc.ipmask) < 0)
+			return sys->sprint("remove address: %r");
+	}
+	bc.ip = nil;
+	bc.ipgw = nil;
+	bc.ipmask = nil;
+	# remote address?
+	# clear net+"/ndb"?
+	return nil;
+}
+
+#
+# the following is just for debugging
+#
+
+dumpdhcp(m: ref Dhcp, dir: string)
+{
+	s := "";
+	sys->print("%s %s/%ud: ", dir, IPaddr.newv6(m.udphdr[Udpraddr:]).text(), get2(m.udphdr, Udprport));
+	if(m.dhcpop != NotDHCP)
+		s = " "+opname(m.dhcpop);
+	sys->print("op %d%s htype %d hops %d xid %ud\n", m.op, s, m.htype, m.hops, m.xid);
+	sys->print("\tsecs %d flags 0x%.4ux\n", m.secs, m.flags);
+	sys->print("\tciaddr %s\n", m.ciaddr.text());
+	sys->print("\tyiaddr %s\n", m.yiaddr.text());
+	sys->print("\tsiaddr %s\n", m.siaddr.text());
+	sys->print("\tgiaddr %s\n", m.giaddr.text());
+	sys->print("\tchaddr ");
+	for(x := 0; x < len m.chaddr; x++)
+		sys->print("%2.2ux", int m.chaddr[x]);
+	sys->print("\n");
+	if(m.sname != nil)
+		sys->print("\tsname %s\n", m.sname);
+	if(m.file != nil)
+		sys->print("\tfile %s\n", m.file);
+	if(m.options != nil){
+		sys->print("\t");
+		printopts(m.options, opts);
+		sys->print("\n");
+	}
+}
+
+Optbytes, Optaddr, Optmask, Optint, Optstr, Optopts, Opthex: con iota;
+
+Opt: adt
+{
+	code:	int;
+	name:	string;
+	otype:	int;
+};
+
+opts: array of Opt = array[] of {
+	(Omask, "ipmask", Optmask),
+	(Orouter, "ipgw", Optaddr),
+	(Odnsserver, "dns", Optaddr),
+	(Ohostname, "hostname", Optstr),
+	(Odomainname, "domain", Optstr),
+	(Ontpserver, "ntp", Optaddr),
+	(Oipaddr, "requestedip", Optaddr),
+	(Olease, "lease", Optint),
+	(Oserverid, "serverid", Optaddr),
+	(Otype, "dhcpop", Optint),
+	(Ovendorclass, "vendorclass", Optstr),
+	(Ovendorinfo, "vendorinfo", Optopts),
+	(Onetbiosns, "wins", Optaddr),
+	(Opop3server, "pop3", Optaddr),
+	(Osmtpserver, "smtp", Optaddr),
+	(Owwwserver, "www", Optaddr),
+	(Oparams, "params", Optbytes),
+	(Otftpserver, "tftp", Optaddr),
+	(Oclientid, "clientid", Opthex),
+};
+
+p9opts: array of Opt = array[] of {
+	(OP9fs, "fs", Optaddr),
+	(OP9auth, "auth", Optaddr),
+};
+
+lookopt(optab: array of Opt, code: int): (int, string, int)
+{
+	for(i:=0; i<len optab; i++)
+		if(opts[i].code == code)
+			return opts[i];
+	return (-1, nil, 0);
+}
+
+printopts(options: list of (int, array of byte), opts: array of Opt)
+{
+	for(; options != nil; options = tl options){
+		(code, val) := hd options;
+		sys->print("(%d %d", code, len val);
+		(nil, name, otype) := lookopt(opts, code);
+		if(name == nil){
+			for(v := 0; v < len val; v++)
+				sys->print(" %d", int val[v]);
+		}else{
+			sys->print(" %s", name);
+			case otype {
+			Optbytes =>
+				for(v := 0; v < len val; v++)
+					sys->print(" %d", int val[v]);
+			Opthex =>
+				for(v := 0; v < len val; v++)
+					sys->print(" %#.2ux", int val[v]);
+			Optaddr or Optmask =>
+				while(len val >= 4){
+					sys->print(" %s", v4text(val));
+					val = val[4:];
+				}
+			Optstr =>
+				sys->print(" \"%s\"", string val);
+			Optint =>
+				n := 0;
+				for(v := 0; v < len val; v++)
+					n = (n<<8) | int val[v];
+				sys->print(" %d", n);
+			Optopts =>
+				printopts(parseopt(val, 0).t1, p9opts);
+			}
+		}
+		sys->print(")");
+	}
+}
--- a/appl/lib/dhcpclient.b	Fri Aug 13 11:37:31 2021
+++ /dev/null	Mon Oct 18 12:05:45 2021
@@ -1,1033 +0,0 @@
-implement Dhcpclient;
-
-#
-# DHCP and BOOTP clients
-# Copyright © 2004-2006 Vita Nuova Holdings Limited
-#
-
-include "sys.m";
-	sys: Sys;
-
-include "ip.m";
-	ip: IP;
-	IPv4off, IPaddrlen, Udphdrlen, Udpraddr, Udpladdr, Udprport, Udplport: import IP;
-	IPaddr: import ip;
-	get2, get4, put2, put4: import ip;
-
-include "keyring.m";
-include "security.m";	# for Random
-
-include "dial.m";
-	dial: Dial;
-
-include "dhcp.m";
-
-debug := 0;
-
-xidgen: int;
-
-init()
-{
-	sys = load Sys Sys->PATH;
-	random := load Random Random->PATH;
-	if(random != nil)
-		xidgen = random->randomint(Random->NotQuiteRandom);
-	else
-		xidgen = sys->pctl(0, nil)*sys->millisec();
-	random = nil;
-	dial = load Dial Dial->PATH;
-	ip = load IP IP->PATH;
-	ip->init();
-}
-
-tracing(d: int)
-{
-	debug = d;
-}
-
-Bootconf.new(): ref Bootconf
-{
-	bc := ref Bootconf;
-	bc.lease = 0;
-	bc.options = array[256] of array of byte;
-	return bc;
-}
-
-Bootconf.get(c: self ref Bootconf, n: int): array of byte
-{
-	a := c.options;
-	if(n & Ovendor){
-		a = c.vendor;
-		n &= ~Ovendor;
-	}
-	if(n < 0 || n >= len a)
-		return nil;
-	return a[n];
-}
-
-Bootconf.getint(c: self ref Bootconf, n: int): int
-{
-	a := c.get(n);
-	v := 0;
-	for(i := 0; i < len a; i++)
-		v = (v<<8) | int a[i];
-	return v;
-}
-
-Bootconf.getip(c: self ref Bootconf, n: int): string
-{
-	l := c.getips(n);
-	if(l == nil)
-		return nil;
-	return hd l;
-}
-
-Bootconf.getips(c: self ref Bootconf, n: int): list of string
-{
-	a := c.get(n);
-	rl: list of string;
-	while(len a >= 4){
-		rl = v4text(a) :: rl;
-		a = a[4:];
-	}
-	l: list of string;
-	for(; rl != nil; rl = tl rl)
-		l = hd rl :: l;
-	return l;
-}
-
-Bootconf.gets(c: self ref Bootconf, n: int): string
-{
-	a := c.get(n);
-	if(a == nil)
-		return nil;
-	for(i:=0; i<len a; i++)
-		if(a[i] == byte 0)
-			break;
-	return string a[0:i];
-}
-
-Bootconf.put(c: self ref Bootconf, n: int, a: array of byte)
-{
-	if(n < 0 || n >= len c.options)
-		return;
-	ca := array[len a] of byte;
-	ca[0:] = a;
-	c.options[n] = ca;
-}
-
-Bootconf.putint(c: self ref Bootconf, n: int, v: int)
-{
-	if(n < 0 || n >= len c.options)
-		return;
-	a := array[4] of byte;
-	put4(a, 0, v);
-	c.options[n] = a;
-}
-
-Bootconf.putips(c: self ref Bootconf, n: int, ips: list of string)
-{
-	if(n < 0 || n >= len c.options)
-		return;
-	na := len ips;
-	a := array[na*4] of byte;
-	na = 0;
-	for(; ips != nil; ips = tl ips){
-		(nil, ipa) := IPaddr.parse(hd ips);
-		a[na++:] = ipa.v4();
-	}
-	c.options[n] = a;
-}
-
-Bootconf.puts(c: self ref Bootconf, n: int, s: string)
-{
-	if(n < 0 || n >= len c.options)
-		return;
-	c.options[n] = array of byte s;
-}
-
-#
-#
-# DHCP
-#
-#
-
-# BOOTP operations
-Bootprequest, Bootpreply: con 1+iota;
-
-# DHCP operations
-NotDHCP, Discover, Offer, Request, Decline, Ack, Nak, Release, Inform: con iota;
-
-Dhcp: adt {
-	udphdr:	array of byte;
-	op:		int;
-	htype:	int;
-	hops:	int;
-	xid:		int;
-	secs:		int;
-	flags:	int;
-	ciaddr:	IPaddr;
-	yiaddr:	IPaddr;
-	siaddr:	IPaddr;
-	giaddr:	IPaddr;
-	chaddr:	array of byte;
-	sname:	string;
-	file:		string;
-	options:	list of (int, array of byte);
-	dhcpop:	int;
-};
-
-opnames := array[] of {
-	Discover => "Discover",
-	Offer => "Offer",
-	Request => "Request",
-	Decline => "Decline",
-	Ack => "Ack",
-	Nak => "Nak",
-	Release => "Release",
-	Inform => "Inform"
-};
-
-opname(op: int): string
-{
-	if(op >= 0 && op < len opnames)
-		return opnames[op];
-	return sys->sprint("OP%d", op);
-}
-
-stringget(buf: array of byte): string
-{
-	for(x := 0; x < len buf; x++)
-		if(buf[x] == byte 0)
-			break;
-	if(x == 0)
-		return nil;
-	return string buf[0 : x];
-}
-
-eqbytes(b1: array of byte, b2: array of byte): int
-{
-	l := len b1;
-	if(l != len b2)
-		return 0;
-	for(i := 0; i < l; i++)
-		if(b1[i] != b2[i])
-			return 0;
-	return 1;
-}
-
-magic := array[] of {byte 99, byte 130, byte 83, byte 99};	# RFC2132 (replacing RFC1048)
-
-dhcpsend(fd: ref Sys->FD, xid: int, dhcp: ref Dhcp)
-{
-	dhcp.xid = xid;
-	abuf := array[576+Udphdrlen] of {* => byte 0};
-	abuf[0:] = dhcp.udphdr;
-	buf := abuf[Udphdrlen:];
-	buf[0] = byte dhcp.op;
-	buf[1] = byte dhcp.htype;
-	buf[2] = byte len dhcp.chaddr;
-	buf[3] = byte dhcp.hops;
-	put4(buf, 4, xid);
-	put2(buf, 8, dhcp.secs);
-	put2(buf, 10, dhcp.flags);
-	buf[12:] = dhcp.ciaddr.v4();
-	buf[16:] = dhcp.yiaddr.v4();
-	buf[20:] = dhcp.siaddr.v4();
-	buf[24:] = dhcp.giaddr.v4();
-	buf[28:] = dhcp.chaddr;
-	buf[44:] = array of byte dhcp.sname;	# [64]
-	buf[108:] = array of byte dhcp.file;	# [128]
-	o := 236;
-	# RFC1542 suggests including magic and Oend as a minimum, even in BOOTP
-	buf[o:] = magic;
-	o += 4;
-	if(dhcp.dhcpop != NotDHCP){
-		buf[o++] = byte Otype;
-		buf[o++] = byte 1;
-		buf[o++] = byte dhcp.dhcpop;
-	}
-	for(ol := dhcp.options; ol != nil; ol = tl ol){
-		(opt, val) := hd ol;
-		buf[o++] = byte opt;
-		buf[o++] = byte len val;
-		if(len val > 0){
-			buf[o:] = val;
-			o += len val;
-		}
-	}
-	buf[o++] = byte Oend;
-	if(debug)
-		dumpdhcp(dhcp, "->");
-	sys->write(fd, abuf, len abuf);
-}
-
-kill(pid: int, grp: string)
-{
-	fd := sys->open("#p/" + string pid + "/ctl", sys->OWRITE);
-	if(fd != nil)
-		sys->fprint(fd, "kill%s", grp);
-}
-
-v4text(a: array of byte): string
-{
-	return sys->sprint("%ud.%ud.%ud.%ud", int a[0], int a[1], int a[2], int a[3]);
-}
-
-parseopt(a: array of byte, isdhcp: int): (int, list of (int, array of byte))
-{
-	opts: list of (int, array of byte);
-	xop := NotDHCP;
-	for(i := 0; i < len a;){
-		op := int a[i++];
-		if(op == Opad)
-			continue;
-		if(op == Oend || i >= len a)
-			break;
-		l := int a[i++];
-		if(i+l > len a)
-			break;
-		if(isdhcp && op == Otype)
-			xop = int a[i];
-		else
-			opts = (op, a[i:i+l]) :: opts;
-		i += l;
-	}
-	rl := opts;
-	opts = nil;
-	for(; rl != nil; rl = tl rl)
-		opts = hd rl :: opts;
-	return (xop, opts);
-}
-
-dhcpreader(pidc: chan of int, srv: ref DhcpIO)
-{
-	pidc <-= sys->pctl(0, nil);
-	for(;;){
-		abuf := array [576+Udphdrlen] of byte;
-		n := sys->read(srv.fd, abuf, len abuf);
-		if(n < 0){
-			if(debug)
-				sys->print("read error: %r\n");
-			sys->sleep(1000);
-			continue;
-		}
-		if(n < Udphdrlen+236){
-			if(debug)
-				sys->print("short read: %d\n", n);
-			continue;
-		}
-		buf := abuf[Udphdrlen:n];
-		n -= Udphdrlen;
-		dhcp := ref Dhcp;
-		dhcp.op = int buf[0];
-		if(dhcp.op != Bootpreply){
-			if(debug)
-				sys->print("bootp: not reply, discarded\n");
-			continue;
-		}
-		dhcp.dhcpop = NotDHCP;
-		if(n >= 240 && eqbytes(buf[236:240], magic))	# otherwise it's something we won't understand
-			(dhcp.dhcpop, dhcp.options) = parseopt(buf[240:n], 1);
-		case dhcp.dhcpop {
-		NotDHCP or Ack or Nak or Offer =>
-			;
-		* =>
-			if(debug)
-				sys->print("dhcp: ignore dhcp op %d\n", dhcp.dhcpop);
-			continue;
-		}
-		dhcp.udphdr = abuf[0:Udphdrlen];
-		dhcp.htype = int buf[1];
-		hlen := int buf[2];
-		dhcp.hops = int buf[3];
-		dhcp.xid = get4(buf, 4);
-		dhcp.secs = get2(buf, 8);
-		dhcp.flags = get2(buf, 10);
-		dhcp.ciaddr = IPaddr.newv4(buf[12:]);
-		dhcp.yiaddr = IPaddr.newv4(buf[16:]);
-		dhcp.siaddr = IPaddr.newv4(buf[20:]);
-		dhcp.giaddr = IPaddr.newv4(buf[24:]);
-		dhcp.chaddr = buf[28 : 28 + hlen];
-		dhcp.sname = stringget(buf[44 : 108]);
-		dhcp.file = stringget(buf[108 : 236]);
-		srv.dc <-= dhcp;
-	}
-}
-
-timeoutstart(msecs: int): (int, chan of int)
-{
-	tc := chan of int;
-	spawn timeoutproc(tc, msecs);
-	return (<-tc, tc);
-}
-
-timeoutproc(c: chan of int, msecs: int)
-{
-	c <-= sys->pctl(0, nil);
-	sys->sleep(msecs);
-	c <-= 1;
-}
-
-hex(b: int): int
-{
-	if(b >= '0' && b <= '9')
-		return b-'0';
-	if(b >= 'A' && b <= 'F')
-		return b-'A' + 10;
-	if(b >= 'a' && b <= 'f')
-		return b-'a' + 10;
-	return -1;
-}
-
-gethaddr(device: string): (int, string, array of byte)
-{
-	fd := sys->open(device, Sys->OREAD);
-	if(fd == nil)
-		return (-1, sys->sprint("%r"), nil);
-	buf := array [100] of byte;
-	n := sys->read(fd, buf, len buf);
-	if(n < 0)
-		return (-1, sys->sprint("%r"), nil);
-	if(n == 0)
-		return (-1, "empty address file", nil);
-	addr := array [n/2] of byte;
-	for(i := 0; i < len addr; i++){
-		u := hex(int buf[2*i]);
-		l := hex(int buf[2*i+1]);
-		if(u < 0 || l < 0)
-			return (-1, "bad address syntax", nil);
-		addr[i] = byte ((u<<4)|l);
-	}
-	return (1, nil, addr);
-}
-
-newrequest(dest: IPaddr, bootfile: string, htype: int, haddr: array of byte, ipaddr: IPaddr, options: array of array of byte): ref Dhcp
-{
-	dhcp := ref Dhcp;
-	dhcp.op = Bootprequest;
-	hdr := array[Udphdrlen] of {* => byte 0};
-	hdr[Udpraddr:] = dest.v6();
-	put2(hdr, Udprport, 67);
-	dhcp.udphdr = hdr;
-	dhcp.htype = htype;
-	dhcp.chaddr = haddr;
-	dhcp.hops = 0;
-	dhcp.secs = 0;
-	dhcp.flags = 0;
-	dhcp.xid = 0;
-	dhcp.ciaddr = ipaddr;
-	dhcp.yiaddr = ip->v4noaddr;
-	dhcp.siaddr = ip->v4noaddr;
-	dhcp.giaddr = ip->v4noaddr;
-	dhcp.file = bootfile;
-	dhcp.dhcpop = NotDHCP;
-	if(options != nil){
-		for(i := 0; i < len options; i++)
-			if(options[i] != nil)
-				dhcp.options = (i, options[i]) :: dhcp.options;
-	}
-	clientid := array[len haddr + 1] of byte;
-	clientid[0] = byte htype;
-	clientid[1:] = haddr;
-	dhcp.options = (Oclientid, clientid) :: dhcp.options;
-	dhcp.options = (Ovendorclass, array of byte "plan9_386") :: dhcp.options;	# 386 will do because type doesn't matter
-	return dhcp;
-}
-
-udpannounce(net: string): (ref Sys->FD, string)
-{
-	if(net == nil)
-		net = "/net";
-	conn := dial->announce(net+"/udp!*!68");
-	if(conn == nil)
-		return (nil, sys->sprint("can't announce dhcp port: %r"));
-	if(sys->fprint(conn.cfd, "headers") < 0)
-		return (nil, sys->sprint("can't set headers mode on dhcp port: %r"));
-	conn.dfd = sys->open(conn.dir+"/data", Sys->ORDWR);
-	if(conn.dfd == nil)
-		return (nil, sys->sprint("can't open %s: %r", conn.dir+"/data"));
-	return (conn.dfd, nil);
-}
-
-ifcnoaddr(fd: ref Sys->FD, s: string)
-{
-	if(fd != nil && sys->fprint(fd, "%s %s %s", s, (ip->noaddr).text(), (ip->noaddr).text()) < 0){
-		if(debug)
-			sys->print("dhcp: ctl %s: %r\n", s);
-	}
-}
-
-setup(net: string, device: string, init: ref Bootconf): (ref Dhcp, ref DhcpIO, string)
-{
-	(htype, err, mac) := gethaddr(device);
-	if(htype < 0)
-		return (nil, nil, sys->sprint("can't get hardware MAC address: %s", err));
-	ciaddr := ip->v4noaddr;
-	if(init != nil && init.ip != nil){
-		valid: int;
-		(valid, ciaddr) = IPaddr.parse(init.ip);
-		if(valid < 0)
-			return (nil, nil, sys->sprint("invalid ip address: %s", init.ip));
-	}
-	(dfd, err2) := udpannounce(net);
-	if(err2 != nil)
-		return (nil, nil, err);
-	bootfile: string;
-	options: array of array of byte;
-	if(init != nil){
-		bootfile = init.bootf;
-		options = init.options;
-	}
-	return (newrequest(ip->v4bcast, bootfile, htype, mac, ciaddr, options), DhcpIO.new(dfd), nil);
-}
-
-#
-# BOOTP (RFC951) is used by Inferno only during net boots, to get initial IP address and TFTP address and parameters
-#
-bootp(net: string, ctlifc: ref Sys->FD, device: string, init: ref Bootconf): (ref Bootconf, string)
-{
-	(req, srv, err) := setup(net, device, init);
-	if(err != nil)
-		return (nil, err);
-	ifcnoaddr(ctlifc, "add");
-	rdhcp := exchange(srv, ++xidgen, req, 1<<NotDHCP);
-	srv.rstop();
-	ifcnoaddr(ctlifc, "remove");
-	if(rdhcp == nil)
-		return (nil, "no response to BOOTP request");
-	return (fillbootconf(init, rdhcp), nil);
-}
-
-defparams := array[] of {
-	byte Omask, byte Orouter, byte Odnsserver, byte Ohostname, byte Odomainname, byte Ontpserver,
-};
-
-#
-# DHCP (RFC2131)
-#
-dhcp(net: string, ctlifc: ref Sys->FD, device: string, init: ref Bootconf, needparam: array of int): (ref Bootconf, ref Lease, string)
-{
-	(req, srv, err) := setup(net, device, init);
-	if(err != nil)
-		return (nil, nil, err);
-	params := defparams;
-	if(needparam != nil){
-		n := len defparams;
-		params = array[n+len needparam] of byte;
-		params[0:] = defparams;
-		for(i := 0; i < len needparam; i++)
-			params[n+i] = byte needparam[i];
-	}
-	initopt := (Oparams, params) :: req.options;	# RFC2131 requires parameters to be repeated each time
-	lease := ref Lease(0, chan[1] of (ref Bootconf, string));
-	spawn dhcp1(srv, lease, net, ctlifc, req, init, initopt);
-	bc: ref Bootconf;
-	(bc, err) = <-lease.configs;
-	return (bc, lease, err);
-}
-
-dhcp1(srv: ref DhcpIO, lease: ref Lease, net: string, ctlifc: ref Sys->FD, req: ref Dhcp, init: ref Bootconf, initopt: list of (int, array of byte))
-{
-	cfd := -1;
-	if(ctlifc != nil)
-		cfd = ctlifc.fd;
-	lease.pid = sys->pctl(Sys->NEWPGRP|Sys->NEWFD, 1 :: srv.fd.fd :: cfd :: nil);
-	if(ctlifc != nil)
-		ctlifc = sys->fildes(ctlifc.fd);
-	srv.fd = sys->fildes(srv.fd.fd);
-	rep: ref Dhcp;
-	ifcnoaddr(ctlifc, "add");
-	if(req.ciaddr.isvalid())
-		rep = reacquire(srv, req, initopt, req.ciaddr);
-	if(rep == nil)
-		rep = askround(srv, req, initopt);
-	srv.rstop();
-	ifcnoaddr(ctlifc, "remove");
-	if(rep == nil){
-		lease.pid = 0;
-		lease.configs <-= (nil, "no response");
-		exit;
-	}
-	for(;;){
-		conf := fillbootconf(init, rep);
-		applycfg(net, ctlifc, conf);
-		if(conf.lease == 0){
-			srv.rstop();
-			lease.pid = 0;
-			flush(lease.configs);
-			lease.configs <-= (conf, nil);
-			exit;
-		}
-		flush(lease.configs);
-		lease.configs <-= (conf, nil);
-		req.ciaddr = rep.yiaddr;
-		while((rep = tenancy(srv, req, conf.lease)) != nil){
-			if(rep.dhcpop == Nak || !rep.ciaddr.eq(req.ciaddr))
-				break;
-			req.udphdr[Udpraddr:] = rep.udphdr[Udpraddr:Udpraddr+IPaddrlen];
-			conf = fillbootconf(init, rep);
-		}
-		removecfg(net, ctlifc, conf);
-		ifcnoaddr(ctlifc, "add");
-		while((rep = askround(srv, req, initopt)) == nil){
-			flush(lease.configs);
-			lease.configs <-= (nil, "no response");
-			srv.rstop();
-			sys->sleep(60*1000);
-		}
-		ifcnoaddr(ctlifc, "remove");
-	}
-}
-
-reacquire(srv: ref DhcpIO, req: ref Dhcp, initopt: list of (int, array of byte), addr: IPaddr): ref Dhcp
-{
-	# INIT-REBOOT: know an address; try requesting it (once)
-	# TO DO: could use Inform when our address is static but we need a few service parameters
-	req.ciaddr = ip->v4noaddr;
-	rep := request(srv, ++xidgen, req, (Oipaddr, addr.v4()) :: initopt);
-	if(rep != nil && rep.dhcpop == Ack && addr.eq(rep.yiaddr)){
-		if(debug)
-			sys->print("req: server accepted\n");
-		req.udphdr[Udpraddr:] = rep.udphdr[Udpraddr:Udpraddr+IPaddrlen];
-		return rep;
-	}
-	if(debug)
-		sys->print("req: cannot reclaim\n");
-	return nil;
-}
-
-askround(srv: ref DhcpIO, req: ref Dhcp, initopt: list of (int, array of byte)): ref Dhcp
-{
-	# INIT
-	req.ciaddr = ip->v4noaddr;
-	req.udphdr[Udpraddr:] = (ip->v4bcast).v6();
-	for(retries := 0; retries < 5; retries++){
-		# SELECTING
-		req.dhcpop = Discover;
-		req.options = initopt;
-		rep := exchange(srv, ++xidgen, req, 1<<Offer);
-		if(rep == nil)
-			break;
-		#
-		# could wait a little while and accumulate offers, but is it sensible?
-		# we do sometimes see arguments between DHCP servers that could
-		# only be resolved by user choice
-		#
-		if(!rep.yiaddr.isvalid())
-			continue;		# server has no idea either
-		serverid := getopt(rep.options, Oserverid, 4);
-		if(serverid == nil)
-			continue;	# broken server
-		# REQUESTING
-		options := (Oserverid, serverid) :: (Oipaddr, rep.yiaddr.v4()) :: initopt;
-		lease := getlease(rep);
-		if(lease != nil)
-			options = (Olease, lease) :: options;
-		rep = request(srv, rep.xid, req, options);
-		if(rep != nil){
-			# could probe with ARP here, and if found, Decline
-			if(debug)
-				sys->print("req: server accepted\n");
-			req.udphdr[Udpraddr:] = rep.udphdr[Udpraddr:Udpraddr+IPaddrlen];
-			return rep;
-		}
-	}
-	return nil;
-}
-
-request(srv: ref DhcpIO, xid: int, req: ref Dhcp, options: list of (int, array of byte)): ref Dhcp
-{
-	req.dhcpop = Request;	# Selecting
-	req.options = options;
-	rep := exchange(srv, xid, req, (1<<Ack)|(1<<Nak));
-	if(rep == nil || rep.dhcpop == Nak)
-		return nil;
-	return rep;
-}
-
-# renew
-#	direct to server from T1 to T2 [RENEW]
-#	Request must not include
-#		requested IP address, server identifier
-#	Request must include
-#		ciaddr set to client's address
-#	Request might include
-#		lease time
-#	similar, but broadcast, from T2 to T3 [REBIND]
-#	at T3, unbind, restart Discover
-
-tenancy(srv: ref DhcpIO, req: ref Dhcp, leasesec: int): ref Dhcp
-{
-	# configure address...
-	t3 := big leasesec * big 1000;	# lease expires; restart
-	t2 := (big 3 * t3)/big 4;	# broadcast renewal request at ¾time
-	t1 := t2/big 2;		# renew lease with original server at ½time
-	srv.rstop();
-	thebigsleep(t1);
-	# RENEW
-	rep := renewing(srv, req, t1, t2);
-	if(rep != nil)
-		return rep;
-	# REBIND
-	req.udphdr[Udpraddr:] = (ip->v4bcast).v6();	# now try broadcast
-	return renewing(srv, req, t2, t3);
-}
-
-renewing(srv: ref DhcpIO, req: ref Dhcp, a: big, b: big): ref Dhcp
-{
-	Minute: con big(60*1000);
-	while(a < b){
-		rep := exchange(srv, req.xid, req, (1<<Ack)|(1<<Nak));
-		if(rep != nil)
-			return rep;
-		delta := (b-a)/big 2;
-		if(delta < Minute)
-			delta = Minute;
-		thebigsleep(delta);
-		a += delta;
-	}
-	return nil;
-}
-
-thebigsleep(msec: big)
-{
-	Day: con big (24*3600*1000);	# 1 day in msec
-	while(msec > big 0){
-		n := msec;
-		if(n > Day)
-			n = Day;
-		sys->sleep(int n);
-		msec -= n;
-	}
-}
-
-getlease(m: ref Dhcp): array of byte
-{
-	lease := getopt(m.options, Olease, 4);
-	if(lease == nil)
-		return nil;
-	if(get4(lease, 0) == 0){
-		lease = array[4] of byte;
-		put4(lease, 0, 15*60);
-	}
-	return lease;
-}
-
-fillbootconf(init: ref Bootconf, pkt: ref Dhcp): ref Bootconf
-{
-	bc := ref Bootconf;
-	if(init != nil)
-		*bc = *init;
-	if(bc.options == nil)
-		bc.options = array[256] of array of byte;
-	for(l := pkt.options; l != nil; l = tl l){
-		(c, v) := hd l;
-		if(bc.options[c] == nil)
-			bc.options[c] = v;	# give priority to first occurring
-	}
-	if((a := bc.get(Ovendorinfo)) != nil){
-		if(bc.vendor == nil)
-			bc.vendor = array[256] of array of byte;
-		for(l = parseopt(a, 0).t1; l  != nil; l = tl l){
-			(c, v) := hd l;
-			if(bc.vendor[c] == nil)
-				bc.vendor[c] = v;
-		}
-	}
-	if(pkt.yiaddr.isvalid()){
-		bc.ip = pkt.yiaddr.text();
-		bc.ipmask = bc.getip(Omask);
-		if(bc.ipmask == nil)
-			bc.ipmask = pkt.yiaddr.classmask().masktext();
-	}
-	bc.bootf = pkt.file;
-	bc.dhcpip = IPaddr.newv6(pkt.udphdr[Udpraddr:]).text();
-	bc.siaddr = pkt.siaddr.text();
-	bc.lease = bc.getint(Olease);
-	if(bc.lease == Infinite)
-		bc.lease = 0;
-	else if(debug > 1)
-		bc.lease = 2*60;	# shorten time, for testing
-	bc.dom = bc.gets(Odomainname);
-	s := bc.gets(Ohostname);
-	for(i:=0; i<len s; i++)
-		if(s[i] == '.'){
-			if(bc.dom == nil)
-				bc.dom = s[i+1:];
-			s = s[0:i];
-			break;
-		}
-	bc.sys = s;
-	bc.ipgw = bc.getip(Orouter);
-	bc.bootip = bc.getip(Otftpserver);
-	bc.serverid = bc.getip(Oserverid);
-	return bc;
-}
-
-Lease.release(l: self ref Lease)
-{
-	# could send a Release message
-	# should unconfigure
-	if(l.pid){
-		kill(l.pid, "grp");
-		l.pid = 0;
-	}
-}
-
-flush(c: chan of (ref Bootconf, string))
-{
-	alt{
-	<-c =>	;
-	* =>	;
-	}
-}
-
-DhcpIO: adt {
-	fd:	ref Sys->FD;
-	pid:	int;
-	dc:	chan of ref Dhcp;
-	new:	fn(fd: ref Sys->FD): ref DhcpIO;
-	rstart:	fn(io: self ref DhcpIO);
-	rstop:	fn(io: self ref DhcpIO);
-};
-
-DhcpIO.new(fd: ref Sys->FD): ref DhcpIO
-{
-	return ref DhcpIO(fd, 0, chan of ref Dhcp);
-}
-
-DhcpIO.rstart(io: self ref DhcpIO)
-{
-	if(io.pid == 0){
-		pids := chan of int;
-		spawn dhcpreader(pids, io);
-		io.pid = <-pids;
-	}
-}
-
-DhcpIO.rstop(io: self ref DhcpIO)
-{
-	if(io.pid != 0){
-		kill(io.pid, "");
-		io.pid = 0;
-	}
-}
-
-getopt(options: list of (int, array of byte), op: int, minlen: int): array of byte
-{
-	for(; options != nil; options = tl options){
-		(opt, val) := hd options;
-		if(opt == op && len val >= minlen)
-			return val;
-	}
-	return nil;
-}
-
-exchange(srv: ref DhcpIO, xid: int, req: ref Dhcp, accept: int): ref Dhcp
-{
-	srv.rstart();
-	nsec := 3;
-	for(count := 0; count < 5; count++) {
-		(tpid, tc) := timeoutstart(nsec*1000);
-		dhcpsend(srv.fd, xid, req);
-	   Wait:
-		for(;;){
-			alt {
-			<-tc=>
-				break Wait;
-			rep := <-srv.dc=>
-				if(debug)
-					dumpdhcp(rep, "<-");
-				if(rep.op == Bootpreply &&
-				    rep.xid == req.xid &&
-				    rep.ciaddr.eq(req.ciaddr) &&
-				    eqbytes(rep.chaddr, req.chaddr)){
-					if((accept & (1<<rep.dhcpop)) == 0){
-						if(debug)
-							sys->print("req: unexpected reply %s to %s\n", opname(rep.dhcpop), opname(req.dhcpop));
-						continue;
-					}
-					kill(tpid, "");
-					return rep;
-				}
-				if(debug)
-					sys->print("req: mismatch\n");
-			}
-		}
-		req.secs += nsec;
-		nsec++;
-	}
-	return nil;
-}
-
-applycfg(net: string, ctlfd: ref Sys->FD, bc: ref Bootconf): string
-{
-	# write addresses to /net/...
-	# local address, mask[or default], remote address [mtu]
-	if(net == nil)
-		net = "/net";
-	if(bc.ip == nil)
-		return  "invalid address";
-	if(ctlfd != nil){
-		if(sys->fprint(ctlfd, "add %s %s", bc.ip, bc.ipmask) < 0)	# TO DO: [raddr [mtu]]
-			return sys->sprint("add interface: %r");
-		# could use "mtu n" request to set/change mtu
-	}
-	# if primary:
-	# 	add default route if gateway valid
-	# 	put ndb entries ip=, ipmask=, ipgw=; sys= dom=; fs=; auth=; dns=; ntp=; other options from bc.options
-	if(bc.ipgw != nil){
-		fd := sys->open(net+"/iproute", Sys->OWRITE);
-		if(fd != nil)
-			sys->fprint(fd, "add 0 0 %s", bc.ipgw);
-	}
-	s := sys->sprint("ip=%s ipmask=%s", bc.ip, bc.ipmask);
-	if(bc.ipgw != nil)
-		s += sys->sprint(" ipgw=%s", bc.ipgw);
-	s += "\n";
-	if(bc.sys != nil)
-		s += sys->sprint("	sys=%s\n", bc.sys);
-	if(bc.dom != nil)
-		s += sys->sprint("	dom=%s.%s\n", bc.sys, bc.dom);
-	if((addr := bc.getip(OP9auth)) != nil)
-		s += sys->sprint("	auth=%s\n", addr);	# TO DO: several addresses
-	if((addr = bc.getip(OP9fs)) != nil)
-		s += sys->sprint("	fs=%s\n", addr);
-	if((addr = bc.getip(Odnsserver)) != nil)
-		s += sys->sprint("	dns=%s\n", addr);
-	fd := sys->open(net+"/ndb", Sys->OWRITE | Sys->OTRUNC);
-	if(fd != nil){
-		a := array of byte s;
-		sys->write(fd, a, len a);
-	}
-	return nil;
-}
-
-removecfg(nil: string, ctlfd: ref Sys->FD, bc: ref Bootconf): string
-{
-	# remove localaddr, localmask[or default]
-	if(ctlfd != nil){
-		if(sys->fprint(ctlfd, "remove %s %s", bc.ip, bc.ipmask) < 0)
-			return sys->sprint("remove address: %r");
-	}
-	bc.ip = nil;
-	bc.ipgw = nil;
-	bc.ipmask = nil;
-	# remote address?
-	# clear net+"/ndb"?
-	return nil;
-}
-
-#
-# the following is just for debugging
-#
-
-dumpdhcp(m: ref Dhcp, dir: string)
-{
-	s := "";
-	sys->print("%s %s/%ud: ", dir, IPaddr.newv6(m.udphdr[Udpraddr:]).text(), get2(m.udphdr, Udprport));
-	if(m.dhcpop != NotDHCP)
-		s = " "+opname(m.dhcpop);
-	sys->print("op %d%s htype %d hops %d xid %ud\n", m.op, s, m.htype, m.hops, m.xid);
-	sys->print("\tsecs %d flags 0x%.4ux\n", m.secs, m.flags);
-	sys->print("\tciaddr %s\n", m.ciaddr.text());
-	sys->print("\tyiaddr %s\n", m.yiaddr.text());
-	sys->print("\tsiaddr %s\n", m.siaddr.text());
-	sys->print("\tgiaddr %s\n", m.giaddr.text());
-	sys->print("\tchaddr ");
-	for(x := 0; x < len m.chaddr; x++)
-		sys->print("%2.2ux", int m.chaddr[x]);
-	sys->print("\n");
-	if(m.sname != nil)
-		sys->print("\tsname %s\n", m.sname);
-	if(m.file != nil)
-		sys->print("\tfile %s\n", m.file);
-	if(m.options != nil){
-		sys->print("\t");
-		printopts(m.options, opts);
-		sys->print("\n");
-	}
-}
-
-Optbytes, Optaddr, Optmask, Optint, Optstr, Optopts, Opthex: con iota;
-
-Opt: adt
-{
-	code:	int;
-	name:	string;
-	otype:	int;
-};
-
-opts: array of Opt = array[] of {
-	(Omask, "ipmask", Optmask),
-	(Orouter, "ipgw", Optaddr),
-	(Odnsserver, "dns", Optaddr),
-	(Ohostname, "hostname", Optstr),
-	(Odomainname, "domain", Optstr),
-	(Ontpserver, "ntp", Optaddr),
-	(Oipaddr, "requestedip", Optaddr),
-	(Olease, "lease", Optint),
-	(Oserverid, "serverid", Optaddr),
-	(Otype, "dhcpop", Optint),
-	(Ovendorclass, "vendorclass", Optstr),
-	(Ovendorinfo, "vendorinfo", Optopts),
-	(Onetbiosns, "wins", Optaddr),
-	(Opop3server, "pop3", Optaddr),
-	(Osmtpserver, "smtp", Optaddr),
-	(Owwwserver, "www", Optaddr),
-	(Oparams, "params", Optbytes),
-	(Otftpserver, "tftp", Optaddr),
-	(Oclientid, "clientid", Opthex),
-};
-
-p9opts: array of Opt = array[] of {
-	(OP9fs, "fs", Optaddr),
-	(OP9auth, "auth", Optaddr),
-};
-
-lookopt(optab: array of Opt, code: int): (int, string, int)
-{
-	for(i:=0; i<len optab; i++)
-		if(opts[i].code == code)
-			return opts[i];
-	return (-1, nil, 0);
-}
-
-printopts(options: list of (int, array of byte), opts: array of Opt)
-{
-	for(; options != nil; options = tl options){
-		(code, val) := hd options;
-		sys->print("(%d %d", code, len val);
-		(nil, name, otype) := lookopt(opts, code);
-		if(name == nil){
-			for(v := 0; v < len val; v++)
-				sys->print(" %d", int val[v]);
-		}else{
-			sys->print(" %s", name);
-			case otype {
-			Optbytes =>
-				for(v := 0; v < len val; v++)
-					sys->print(" %d", int val[v]);
-			Opthex =>
-				for(v := 0; v < len val; v++)
-					sys->print(" %#.2ux", int val[v]);
-			Optaddr or Optmask =>
-				while(len val >= 4){
-					sys->print(" %s", v4text(val));
-					val = val[4:];
-				}
-			Optstr =>
-				sys->print(" \"%s\"", string val);
-			Optint =>
-				n := 0;
-				for(v := 0; v < len val; v++)
-					n = (n<<8) | int val[v];
-				sys->print(" %d", n);
-			Optopts =>
-				printopts(parseopt(val, 0).t1, p9opts);
-			}
-		}
-		sys->print(")");
-	}
-}
--- /dev/null	Mon Oct 18 12:05:45 2021
+++ b/appl/lib/dhcpd.b	Fri Aug 13 08:24:53 2021
@@ -0,0 +1,375 @@
+implement Dhcpserver;
+
+include "sys.m";
+	sys: Sys;
+	sprint: import sys;
+include "ip.m";
+	ip: IP;
+	IPaddr: import ip;
+include "encoding.m";
+	base16: Encoding;
+include "lists.m";
+	lists: Lists;
+include "ether.m";
+	ether: Ether;
+include "dhcpd.m";
+
+debug: con 0;
+
+tdnames := array[] of {
+	nil, "discover", "offer", "request", "decline", "ack", "nak", "release", "inform",
+};
+
+# option value types, for converting to text
+OTstr, OTips, OTint16, OTint32, OTother: con iota;
+optstring := array[] of {
+	Ohostname, Odomainname, Orootpath, Omessage, Otftpservername, Obootfile,
+};
+optips := array[] of {
+	Osubnetmask, Orouters, Odns, Obroadcast, Oreqipaddr, Oserverid,
+};
+optint16 := array[] of {
+	Omaxmsgsize,
+};
+optint32 := array[] of {
+	Oleasetime, Orenewaltime, Orebindingtime,
+};
+
+optnames := array[] of {
+Opad		=> "pad",
+Oend		=> "end",
+Osubnetmask	=> "ipmask",
+Orouters	=> "ipgw",
+Odns		=> "dns",
+Ohostname	=> "sys",
+Odomainname	=> "dnsdomain",
+Orootpath	=> "rootpath",
+Obroadcast	=> "ipbroadcast",
+
+Oreqipaddr	=> "reqipaddr",
+Oleasetime	=> "leasetime",
+Ooptionoverload => "optionoverload",
+Odhcpmsgtype	=> "dhcpmsgtype",
+Oserverid	=> "serverid",
+Oparamreq	=> "paramreq",
+Omessage	=> "message",
+Omaxmsgsize	=> "maxmsgsize",
+Orenewaltime	=> "renewaltime",
+Orebindingtime	=> "rebindingtime",
+Ovendorclass	=> "vendorclass",
+Oclientid	=> "clientid",
+Otftpservername => "tftp",
+Obootfile	=> "bootf",
+};
+
+
+init()
+{
+	sys = load Sys Sys->PATH;
+	ip = load IP IP->PATH;
+	ip->init();
+	base16 = load Encoding Encoding->BASE16PATH;
+	lists = load Lists Lists->PATH;
+	ether = load Ether Ether->PATH;
+	ether->init();
+}
+
+optsparse(d: array of byte): list of ref Opt
+{
+	if(g32(d, 0).t0 != Moptions)
+		return nil;
+
+	l: list of ref Opt;
+	o := 4;
+	while(o < len d) {
+		code := int d[o++];
+		if(code == Oend)
+			break;
+		if(code == Opad)
+			continue;
+		if(o >= len d) {
+			say("bad options, length outside packet");
+			return nil;
+		}
+		n := int d[o++];
+		if(o+n > len d) {
+			say("bad options, value outside packet");
+			return nil;
+		}
+		l = ref Opt(code, d[o:o+n])::l;
+		o += n;
+	}
+	return lists->reverse(l);
+}
+
+optssize(l: list of ref Opt): int
+{
+	o := 4;
+	for(; l != nil; l = tl l)
+		case (hd l).code {
+		Opad or
+		Oend =>
+			o += 1;
+		* =>
+			o += 1+1+len (hd l).v;
+		}
+	return o;
+}
+
+optspack(l: list of ref Opt, buf: array of byte)
+{
+	if(l == nil)
+		return;
+
+	o := 0;
+	o = p32(buf, o, Moptions);
+	for(; l != nil; l = tl l) {
+		opt := hd l;
+		if(o >= len buf)
+			return warn("response options too long (code)");
+		buf[o++] = byte opt.code;
+		if(opt.code == Opad)
+			continue;
+		if(opt.code == Oend) {
+			if(len l != 1)
+				raise "option 'end' not last";
+			break;
+		}
+
+		if(o+1+len opt.v > len buf)
+			return warn("response options too long (len/value)");
+		buf[o++] = byte len opt.v;
+		buf[o:] = opt.v;
+		o += len opt.v;
+	}
+}
+
+optname(code: int): string
+{
+	if(code >= 0 && code < len optnames)
+		s := optnames[code];
+	if(s == nil)
+		s = string code;
+	return s;
+}
+
+has(a: array of int, v: int): int
+{
+	for(i := 0; i < len a; i++)
+		if(a[i] == v)
+			return 1;
+	return 0;
+}
+
+opttype(code: int): int
+{
+	if(has(optstring, code))
+		return OTstr;
+	if(has(optips, code))
+		return OTips;
+	if(has(optint16, code))
+		return OTint16;
+	if(has(optint32, code))
+		return OTint32;
+	return OTother;
+}
+
+optstext(l: list of ref Opt): string
+{
+	s := "";
+	for(; l != nil; l = tl l) {
+		o := hd l;
+		s += "\t\t"+optname(o.code);
+		if(o.v == nil) {
+			s += "\n";
+			continue;
+		}
+		s += "=";
+		ot := opttype(o.code);
+		if(o.code == Odhcpmsgtype && len o.v == 1 && (t := int o.v[0]) >= 0 && t < len tdnames && tdnames[t] != nil) {
+			s += sprint("%s", tdnames[t]);
+		} else if(ot == OTstr) {
+			s += sprint("\"%s\"", string o.v);
+		} else if(ot == OTips && len o.v % 4 == 0) {
+			v := o.v;
+			ips := "";
+			while(len v >= 4) {
+				ips += ","+IPaddr.newv4(v[:4]).text();
+				v = v[4:];
+			}
+			if(ips != nil)
+				ips = ips[1:];
+			s += sprint("%s", ips);
+		} else if(ot == OTint16 && len o.v == 2) {
+			s += sprint("%d", g16(o.v, 0).t0);
+		} else if(ot == OTint32 && len o.v == 4) {
+			s += sprint("%d", g32(o.v, 0).t0);
+		} else {
+			s += sprint("'%s'", base16->enc(o.v));
+		}
+		s += "\n";
+	}
+	return s;
+}
+
+Dhcpmsg.unpack(buf: array of byte): (ref Dhcpmsg, string)
+{
+	if(len buf < Minmsglen)
+		return (nil, sprint("too short, %d < minimum %d", len buf, Minmsglen));
+
+	m := ref Dhcpmsg;
+	o := 0;
+	m.op = int buf[o++];
+	m.htype = int buf[o++];
+	m.hlen = int buf[o++];
+	if(m.hlen > 16)
+		return (nil, sprint("bad hlen %d", m.hlen));
+	m.hops = int buf[o++];
+	(m.xid, o) = g32(buf, o);
+	(m.secs, o) = g16(buf, o);
+	(m.flags, o) = g16(buf, o);
+	m.ciaddr = IPaddr.newv4(buf[o:o+4]);
+	o += 4;
+	m.yiaddr = IPaddr.newv4(buf[o:o+4]);
+	o += 4;
+	m.siaddr = IPaddr.newv4(buf[o:o+4]);
+	o += 4;
+	m.giaddr = IPaddr.newv4(buf[o:o+4]);
+	o += 4;
+	m.chaddr = buf[o:o+m.hlen];
+	o += 16;
+	m.sname = cstr(buf[o:o+64]);
+	o += 64;
+	m.file = cstr(buf[o:o+128]);
+	o += 128;
+	m.options = buf[o:];
+	m.opts = optsparse(m.options);
+	return (m, nil);
+}
+
+Dhcpmsg.pack(m: self ref Dhcpmsg): array of byte
+{
+	size := 7*4+16+64+128;
+
+	# for dhcp, no minimum length.  bootp needs 64 bytes options.
+	optsize := len m.options;
+	if(len m.options >= 4 && g32(m.options, 0).t0 == Moptions)
+		optsize = optssize(m.opts);
+	if(0 && optsize < 64)
+		optsize = 64;
+	size += optsize;
+
+	buf := array[size] of {* => byte 0};
+	o := 0;
+	buf[o++] = byte m.op;
+	buf[o++] = byte m.htype;
+	buf[o++] = byte m.hlen;
+	buf[o++] = byte m.hops;
+	o = p32(buf, o, m.xid);
+	o = p16(buf, o, m.secs);
+	o = p16(buf, o, m.flags);
+	o = pbuf(buf, o, m.ciaddr.v4());
+	o = pbuf(buf, o, m.yiaddr.v4());
+	o = pbuf(buf, o, m.siaddr.v4());
+	o = pbuf(buf, o, m.giaddr.v4());
+	buf[o:] = m.chaddr;
+	o += 16;
+	buf[o:] = array of byte m.sname;
+	o += 64;
+	buf[o:] = array of byte m.file;
+	o += 128;
+	if(len m.options >= 4 && g32(m.options, 0).t0 == Moptions)
+		optspack(m.opts, buf[o:]);
+	else if(m.options != nil)
+		buf[o:] = m.options;
+	return buf;
+}
+
+Dhcpmsg.text(m: self ref Dhcpmsg): string
+{
+	s := "Dhcpmsg(\n";
+	s += sprint("	op=%d\n", int m.op);
+	s += sprint("	htype=%d\n", m.htype);
+	s += sprint("	hlen=%d\n", m.hlen);
+	s += sprint("	hops=%d\n", m.hops);
+	s += sprint("	xid=%ud\n", m.xid);
+	s += sprint("	secs=%ud\n", m.secs);
+	s += sprint("	ciaddr=%s\n", m.ciaddr.text());
+	s += sprint("	yiaddr=%s\n", m.yiaddr.text());
+	s += sprint("	siaddr=%s\n", m.siaddr.text());
+	s += sprint("	giaddr=%s\n", m.giaddr.text());
+	s += sprint("	hwaddr=%q\n", ether->text(m.chaddr));
+	s += sprint("	sname=%q\n", m.sname);
+	s += sprint("	file=%q\n", m.file);
+	if(len m.options >= 4) {
+		magic := g32(m.options, 0).t0;
+		s += sprint("	magic=%#ux\n", magic);
+		if(magic == Moptions) {
+			s += "\toptions=(\n";
+			s += optstext(m.opts);
+			s += "\t)\n";
+		} else if (magic == Mp9)
+			s += sprint("\toptions=%q\n", cstr(m.options[4:]));
+	}
+	s += ")";
+	return s;
+}
+
+cstr(buf: array of byte): string
+{
+	for(i := 0; i < len buf; i++)
+		if(buf[i] == byte 0)
+			break;
+	return string buf[:i];
+}
+
+g32(d: array of byte, o: int): (int, int)
+{
+	v := 0;
+	v |= int d[o++]<<24;
+	v |= int d[o++]<<16;
+	v |= int d[o++]<<8;
+	v |= int d[o++]<<0;
+	return (v, o);
+}
+
+g16(d: array of byte, o: int): (int, int)
+{
+	v := 0;
+	v |= int d[o++]<<8;
+	v |= int d[o++]<<0;
+	return (v, o);
+}
+
+p32(d: array of byte, o: int, v: int): int
+{
+	d[o++] = byte (v>>24);
+	d[o++] = byte (v>>16);
+	d[o++] = byte (v>>8);
+	d[o++] = byte (v>>0);
+	return o;
+}
+
+p16(d: array of byte, o: int, v: int): int
+{
+	d[o++] = byte (v>>8);
+	d[o++] = byte (v>>0);
+	return o;
+}
+
+pbuf(d: array of byte, o: int, buf: array of byte): int
+{
+	d[o:] = buf;
+	return o+len buf;
+}
+
+warn(s: string)
+{
+	say(s);
+}
+
+say(s: string)
+{
+	if(debug)
+		sys->fprint(sys->fildes(2), "%s\n", s);
+}
--- /dev/null	Mon Oct 18 12:05:45 2021
+++ b/appl/lib/ipval.b	Fri Aug 13 08:24:53 2021
@@ -0,0 +1,157 @@
+implement Ipval;
+
+include "sys.m";
+	sys: Sys;
+	sprint: import sys;
+include "bufio.m";
+include "ip.m";
+	ip: IP;
+	IPaddr: import ip;
+include "attrdb.m";
+	attrdb: Attrdb;
+	Db, Dbptr, Dbentry: import attrdb;
+include "ipval.m";
+
+
+init()
+{
+	if(sys != nil)
+		return;
+
+	sys = load Sys Sys->PATH;
+	ip = load IP IP->PATH;
+	ip->init();
+	attrdb = load Attrdb Attrdb->PATH;
+	attrdb->init();
+	Db.open("/dev/null");  # force bufio to load...
+}
+
+findval(db: ref Db, ipaddr: string, rattr: string): (string, string)
+{
+	init();
+
+	(l, err) := findvals(db, ipaddr, rattr::nil);
+	v: string;
+	if(err == nil && len l == 1)
+		v = (hd l).t1;
+	return (v, err);
+}
+
+has(l: list of (string, string), k: string): int
+{
+	for(; l != nil; l = tl l)
+		if((hd l).t0 == k)
+			return 1;
+	return 0;
+}
+
+dbipval(db: ref Db, ipaddr: string, attr: string): string
+{
+	(e, nil) := db.findbyattr(nil, "ip", ipaddr, attr);
+	if(e != nil)
+		v := e.findfirst(attr);
+	return v;
+}
+
+dbipnetmatch(e: ref Dbentry, ipa: IPaddr): array of byte
+{
+	eipaddr := e.findfirst("ip");
+	eipmask := e.findfirst("ipmask");
+	(ok0, eipa) := IPaddr.parse(eipaddr);
+	(ok1, eipm) := IPaddr.parsemask(eipmask);
+	if(ok0 < 0 || ok1 < 0)
+		return nil;
+	if(!ipa.mask(eipm).eq(eipa))
+		return nil;
+	return eipm.v6();
+}
+
+# compare the mask.  a >= b when a's mask is less specific than b's:  when the first different byte of the mask has a lower value.
+# if the masks are the same, we use the position in the ndb file.
+v6maskge(a, b: ref (int, array of byte, ref Dbentry)): int
+{
+	ma := a.t1;
+	mb := b.t1;
+
+	for(i := 0; i < len ma; i++)
+		if(ma[i] != mb[i])
+			return ma[i] <= mb[i];
+	return a.t0 >= b.t0;
+}
+
+dbipnets(db: ref Db, ipaddr: string): array of ref Dbentry
+{
+	l: list of ref (int, array of byte, ref Dbentry);  # seq, v6 mask, db entry
+
+	(ok, ipa) := IPaddr.parse(ipaddr);
+	if(ok < 0)
+		return nil;
+
+	next: ref Dbptr;
+	seq := 0;
+	for(;;) {
+		e: ref Dbentry;
+		(e, next) = db.find(next, "ipnet");
+		if(e != nil) {
+			v6m := dbipnetmatch(e, ipa);
+			if(v6m != nil)
+				l = ref (seq++, v6m, e)::l;
+		}
+		if(e == nil || next == nil)
+			break;
+	}
+
+	# use seq & v6 mask to sort, then extract db entries
+	a := l2a(l);
+	sort(a, v6maskge);
+	r := array[len a] of ref Dbentry;
+	for(i := 0; i < len a; i++)
+		r[i] = a[i].t2;
+	return r;
+}
+
+findvals(db: ref Db, ipaddr: string, rattrs: list of string): (list of (string, string), string)
+{
+	init();
+
+	r: list of (string, string);
+
+	# first look for explict matches to ip address
+	for(l := rattrs; l != nil; l = tl l) {
+		v := dbipval(db, ipaddr, hd l);
+		if(v != nil)
+			r = (hd l, v)::r;
+	}
+
+	# take remaining attributes from matching ipnet entries
+	ipn := dbipnets(db, ipaddr);
+	for(i := 0; len r < len rattrs && i < len ipn; i++) {
+		dbe := ipn[i];
+		for(l = rattrs; l != nil; l = tl l) {
+			v := dbe.findfirst(hd l);
+			if(v != nil && !has(r, hd l))
+				r = (hd l, v)::r;
+		}
+	}
+
+	return (r, nil);
+}
+
+l2a[T](l: list of T): array of T
+{
+	a := array[len l] of T;
+	i := 0;
+	for(; l != nil; l = tl l)
+		a[i++] = hd l;
+	return a;
+}
+
+sort[T](a: array of T, ge: ref fn(a, b: T): int)
+{
+	for(i := 1; i < len a; i++) {
+		tmp := a[i];
+		for(j := i; j > 0 && ge(a[j-1], tmp); j--)
+			a[j] = a[j-1];
+		a[j] = tmp;
+	}
+}
--- a/appl/lib/mkfile	Fri Aug 13 11:37:31 2021
+++ b/appl/lib/mkfile	Fri Aug 13 08:24:53 2021
@@ -36,7 +36,8 @@
 	debug.dis\
 	deflate.dis\
 	devpointer.dis\
-	dhcpclient.dis\
+	dhcp.dis\
+	dhcpd.dis\
 	dial.dis\
 	dialog.dis\
 	dict.dis\
@@ -60,6 +61,7 @@
 	iobuf.dis\
 	ip.dis\
 	ipattr.dis\
+	ipval.dis\
 	ir.dis\
 	irsage.dis\
 	irsim.dis\
@@ -168,6 +170,8 @@
 	db.m\
 	debug.m\
 	devpointer.m\
+	dhcp.m\
+	dhcpd.m\
 	dict.m\
 	draw.m\
 	env.m\
@@ -180,6 +184,7 @@
 	html.m\
 	imagefile.m\
 	inflate.m\
+	ipval.m\
 	ir.m\
 	keyring.m\
 	lock.m\
--- /dev/null	Mon Oct 18 12:05:45 2021
+++ b/man/8/dhcpd	Fri Aug 13 08:24:53 2021
@@ -0,0 +1,118 @@
+.TH DHCPD 8
+.SH NAME
+.B ip/dhcpd
+[
+.B -ds
+] [
+.B -f
+.I ndbfile
+] [
+.B -x
+.I net
+] [
+.I ipaddr n ...
+]
+.SH DESCRIPTION
+.B Dhcpd
+serves the BOOTP protocol, and DHCP extensions.
+.PP
+Options:
+.TP
+.BI -f " ndbfile"
+Read configuration from
+.I ndbfile
+instead of the default
+.IR /lib/ndb/local .
+.TP
+.BI -x " net"
+Serve on
+.I net
+instead of the default
+.IR /net .
+.TP
+.B -d
+Print debug information.
+.TP
+.B -s
+Only listen for and print requests, don't respond to them.
+.PP
+Bootp clients need an explicit
+.I ether
+entry in the ndb file.
+Dhcp clients may have an explicit
+.I ether
+entry.  Clients that are not explicitly configured will receive an available address from the dynamic address pool.
+The pool is specified by parameters to dhcpd, with zero or more pairs of
+.I ipaddr
+and
+.IR n ,
+the first ip address of a range and the number of addresses.
+Leases are recorded in
+.IR /services/dhcpd/,
+each leased ip address is stored in a file named after the ip address.
+Files in /services/dhcpd/ are read at start up, to ensure leases survive restarts
+dhcpd and clients receive the same ip addresses as long
+as the pool has unused addresses.
+.PP
+Clients request an ip address and accompanying configuration (e.g.
+gateway, dns servers).  Dhcpd consults the ndb file for the following
+options:
+.IR ipmask ,
+.IR ipgw,
+.IR dns ,
+.IR sys ,
+.IR dnsdomain ,
+.IR bootf ,
+.IR rootpath ,
+.IR leasetime ,
+.IR nextserver .
+.IR Ndb (6)
+defines most of them.
+.I Rootpath
+is used by clients with a network file system as root.
+.I Leasetime
+is the DHCP lease time, it defaults to one day.
+.I Nextserver
+is the server to continue booting from, typically a tftp server that serves a kernel.
+.PP
+Attributes are resolved by first looking for entries that exactly match the IP address.
+.I Ipnet
+entries are consulted for the remaining options, most specific
+.I ipmask
+first.
+.SH EXAMPLE
+The following is a snippet from a /lib/ndb/local:
+
+.EX
+	ipnet=lan ip=192.168.1.0 ipmask=255.255.255.0
+		ipgw=192.168.1.254
+		dnsdomain=lan
+		dns=192.168.1.254
+		nextserver=192.168.1.1
+
+	ip=192.168.1.1 sys=narf
+	ip=192.168.1.10 sys=laser
+	ip=192.168.1.11 sys=wrt54gl
+
+	ip=192.168.1.33 ether=000ae43623bc sys=dis
+	ip=192.168.1.34 ether=000ae4269c38 sys=bad
+		bootf=9pxeload
+	ip=192.168.1.35 ether=000ae432664c sys=yeah
+	ip=192.168.1.50 ether=00504301c7d5 sys=sheeva0
+		bootf=sheeva0
+.EE
+.SH SOURCE
+.B /appl/cmd/ip/dhcpd.b
+.SH FILES
+.B /services/dhcpd/
+.SH SEE ALSO
+.IR ndb (6),
+.IR dhcp (8)
+.br
+Internet RFCs
+.IR RFC951 ,
+.IR RFC1542 ,
+.IR RFC2131 ,
+.IR RFC2132
+.SH BUGS
+Only DHCPv4 support is implemented.
--- /dev/null	Mon Oct 18 12:05:45 2021
+++ b/module/dhcpd.m	Fri Aug 13 08:24:53 2021
@@ -0,0 +1,72 @@
+Dhcpserver: module
+{
+	PATH:	con "/dis/lib/dhcpd.dis";
+
+	init:	fn();
+
+	Minmsglen:	con 7*8+16+64+128;  # dhcp clients may have no options
+
+	# flags
+	Fbroadcast:	con 1<<15;
+
+	Mp9:		con 16r70392020; # "p9  "
+	Moptions:	con 16r63825363; # rfc2132
+
+	# bootp/dhcp
+	Opad:	con 0;
+	Oend:	con 255;
+	Osubnetmask:	con 1;
+	Orouters:	con 3;
+	Odns:		con 6;
+	Ohostname:	con 12;
+	Odomainname:	con 15;
+	Orootpath:	con 17;
+	Obroadcast:	con 28;
+	# dhcp only
+	Oreqipaddr,
+	Oleasetime,
+	Ooptionoverload,
+	Odhcpmsgtype,
+	Oserverid,
+	Oparamreq,
+	Omessage,
+	Omaxmsgsize,
+	Orenewaltime,
+	Orebindingtime,
+	Ovendorclass,
+	Oclientid:	con 50+iota;
+	Otftpservername,
+	Obootfile:	con 66+iota;
+
+	Trequest, Treply: con 1+iota;
+	TDdiscover, TDoffer, TDrequest, TDdecline, TDack, TDnak, TDrelease, TDinform: con 1+iota;
+
+	Opt: adt {
+		code:	int;
+		v:	array of byte;
+	};
+
+	Dhcpmsg: adt
+	{
+		op:	int;
+		htype:	int;
+		hlen:	int;
+		hops:	int;
+		xid:	int;
+		secs:	int;
+		flags:	int;
+		ciaddr:	IPaddr;
+		yiaddr:	IPaddr;
+		siaddr:	IPaddr;
+		giaddr:	IPaddr;
+		chaddr:	array of byte;
+		sname:	string;
+		file:	string;
+		options:	array of byte;
+		opts:	list of ref Opt;
+
+		unpack:	fn(a: array of byte): (ref Dhcpmsg, string);
+		pack:	fn(bp: self ref Dhcpmsg): array of byte;
+		text:	fn(bp: self ref Dhcpmsg): string;
+	};
+};
--- /dev/null	Mon Oct 18 12:05:45 2021
+++ b/module/ipval.m	Fri Aug 13 08:24:53 2021
@@ -0,0 +1,7 @@
+Ipval: module
+{
+	PATH:	con "/dis/lib/ipval.dis";
+
+	findval:	fn(db: ref Attrdb->Db, ipaddr: string, rattr: string): (string, string);
+	findvals:	fn(db: ref Attrdb->Db, ipaddr: string, rattrs: list of string): (list of (string, string), string);
+};