shithub: 9ferno

Download patch

ref: 51fdecc65a1ae47d7c8219828af7c990884e159f
parent: 58055b9e3a8c6df1cd128a3d4ed3667eab94e472
author: 9ferno <gophone2015@gmail.com>
date: Sun Aug 15 08:19:51 EDT 2021

changed dhcp module names

--- a/appl/cmd/ip/dhcp.b	Sat Aug 14 10:59:42 2021
+++ b/appl/cmd/ip/dhcp.b	Sun Aug 15 08:19:51 2021
@@ -15,7 +15,7 @@
 	IPaddr: import ip;
 	get2, get4, put2, put4: import ip;
 
-include "dhcp.m";
+include "dhcpclient.m";
 	dhcpclient: Dhcpclient;
 	Bootconf, Lease: import dhcpclient;
 
@@ -56,7 +56,7 @@
 		'm' =>	monitor = 1;
 		'n' =>	noctl = 1;
 		'p' =>	pcfg = 1;
-		'r' =>		retry = 1;
+		'r' =>	retry = 1;
 		'x' =>	netdir = arg->earg();
 		* =>		arg->usage();
 		}
--- a/appl/cmd/ip/dhcpd.b	Sat Aug 14 10:59:42 2021
+++ b/appl/cmd/ip/dhcpd.b	Sun Aug 15 08:19:51 2021
@@ -24,7 +24,7 @@
 	lists: Lists;
 include "encoding.m";
 	base16: Encoding;
-include "dhcpd.m";
+include "dhcpserver.m";
 	dhcp: Dhcpserver;
 	Dhcpmsg, Opt: import dhcp;
 include "ipval.m";
--- a/appl/cmd/ip/mkfile	Sat Aug 14 10:59:42 2021
+++ b/appl/cmd/ip/mkfile	Sun Aug 15 08:19:51 2021
@@ -19,7 +19,8 @@
 	attrdb.m\
 	bufio.m\
 	daytime.m\
-	dhcp.m\
+	dhcpclient.m\
+	dhcpserver.m\
 	draw.m\
 	encoding.m\
 	ether.m\
--- a/appl/lib/dhcp.b	Sat Aug 14 10:59:42 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/dhcpclient.b	Sun Aug 15 08:19:51 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 "dhcpclient.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/dhcpd.b	Sat Aug 14 10:59:42 2021
+++ /dev/null	Mon Oct 18 12:05:45 2021
@@ -1,375 +0,0 @@
-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/dhcpserver.b	Sun Aug 15 08:19:51 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 "dhcpserver.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);
+}
--- a/appl/lib/mkfile	Sat Aug 14 10:59:42 2021
+++ b/appl/lib/mkfile	Sun Aug 15 08:19:51 2021
@@ -36,8 +36,8 @@
 	debug.dis\
 	deflate.dis\
 	devpointer.dis\
-	dhcp.dis\
-	dhcpd.dis\
+	dhcpclient.dis\
+	dhcpserver.dis\
 	dial.dis\
 	dialog.dis\
 	dict.dis\
@@ -170,8 +170,8 @@
 	db.m\
 	debug.m\
 	devpointer.m\
-	dhcp.m\
-	dhcpd.m\
+	dhcpclient.m\
+	dhcpserver.m\
 	dict.m\
 	draw.m\
 	env.m\
@@ -239,7 +239,8 @@
 names.dis: $ROOT/module/names.m
 disks.dis: $ROOT/module/disks.m
 scsiio.dis:	$ROOT/module/scsiio.m
-dhcpclient.dis:	$ROOT/module/dhcp.m
+dhcpclient.dis:	$ROOT/module/dhcpclient.m
+dhcpserver.dis:	$ROOT/module/dhcpserver.m
 ubfa.dis: $ROOT/module/ubfa.m
 secstore.dis:	$ROOT/module/secstore.m
 ida.dis:	$ROOT/module/ida.m
--- a/module/dhcp.m	Sat Aug 14 10:59:42 2021
+++ /dev/null	Mon Oct 18 12:05:45 2021
@@ -1,84 +0,0 @@
-Dhcpclient: module
-{
-	PATH: con "/dis/lib/dhcpclient.dis";
-
-	Bootconf: adt {
-		ip:	string;
-		ipgw:	string;
-		ipmask:	string;
-		bootf:	string;
-		bootip:	string;
-		dhcpip:	string;
-		siaddr:	string;
-		serverid:	string;
-		sys:	string;
-		dom:	string;
-		lease:	int;
-		options:	array of array of byte;
-		vendor:	array of array of byte;
-
-		new:	fn(): ref Bootconf;
-		get:	fn(c: self ref Bootconf, n: int): array of byte;
-		getint:	fn(c: self ref Bootconf, n: int): int;
-		getip:	fn(c: self ref Bootconf, n: int): string;
-		getips:	fn(c: self ref Bootconf, n: int): list of string;
-		gets:	fn(c: self ref Bootconf, n: int): string;
-		put:	fn(c: self ref Bootconf, n: int, a: array of byte);
-		putint:	fn(c: self ref Bootconf, n: int, v: int);
-		putips:	fn(c: self ref Bootconf, n: int, ips: list of string);
-		puts:	fn(c: self ref Bootconf, n: int, s: string);
-	};
-
-	Lease: adt {
-		pid:	int;
-		configs:	chan of (ref Bootconf, string);
-
-		release:	fn(l: self ref Lease);
-	};
-
-	init:	fn();
-	tracing:	fn(debug: int);
-	bootp:	fn(net: string, ctlifc: ref Sys->FD, device: string, init: ref Bootconf): (ref Bootconf, string);
-	dhcp:	fn(net: string, ctlifc: ref Sys->FD, device: string, init: ref Bootconf, options: array of int): (ref Bootconf, ref Lease, string);
-
-	applycfg:	fn(net: string, ctlifc: ref Sys->FD, conf: ref Bootconf): string;
-	removecfg:	fn(net: string, ctlifc: ref Sys->FD, conf: ref Bootconf): string;
-
-	# bootp options used here
-	Opad: con 0;
-	Oend: con 255;
-	Omask: con 1;
-	Orouter: con 3;
-	Odnsserver: con 6;
-	Ocookieserver: con 8;
-	Ohostname: con 12;
-	Odomainname: con 15;
-	Ontpserver: con 42;
-	Ovendorinfo: con 43;
-	Onetbiosns: con 44;
-	Osmtpserver: con 69;
-	Opop3server: con 70;
-	Owwwserver: con 72;
-
-	# dhcp options
-	Oipaddr: con 50;
-	Olease: con 51;
-	Ooverload: con 52;
-	Otype: con 53;
-	Oserverid: con 54;
-	Oparams: con 55;
-	Omessage: con 56;
-	Omaxmsg: con 57;
-	Orenewaltime: con 58;
-	Orebindingtime: con 59;
-	Ovendorclass: con 60;
-	Oclientid: con 61;
-	Otftpserver: con 66;
-	Obootfile: con 67;
-
-	Ovendor:	con (1<<8);
-	OP9fs: con Ovendor|128;	# plan 9 file server
-	OP9auth:	con Ovendor|129;	# plan 9 auth server
-
-	Infinite:	con ~0;	# lease
-};
--- /dev/null	Mon Oct 18 12:05:45 2021
+++ b/module/dhcpclient.m	Sun Aug 15 08:19:51 2021
@@ -0,0 +1,84 @@
+Dhcpclient: module
+{
+	PATH: con "/dis/lib/dhcpclient.dis";
+
+	Bootconf: adt {
+		ip:	string;
+		ipgw:	string;
+		ipmask:	string;
+		bootf:	string;
+		bootip:	string;
+		dhcpip:	string;
+		siaddr:	string;
+		serverid:	string;
+		sys:	string;
+		dom:	string;
+		lease:	int;
+		options:	array of array of byte;
+		vendor:	array of array of byte;
+
+		new:	fn(): ref Bootconf;
+		get:	fn(c: self ref Bootconf, n: int): array of byte;
+		getint:	fn(c: self ref Bootconf, n: int): int;
+		getip:	fn(c: self ref Bootconf, n: int): string;
+		getips:	fn(c: self ref Bootconf, n: int): list of string;
+		gets:	fn(c: self ref Bootconf, n: int): string;
+		put:	fn(c: self ref Bootconf, n: int, a: array of byte);
+		putint:	fn(c: self ref Bootconf, n: int, v: int);
+		putips:	fn(c: self ref Bootconf, n: int, ips: list of string);
+		puts:	fn(c: self ref Bootconf, n: int, s: string);
+	};
+
+	Lease: adt {
+		pid:	int;
+		configs:	chan of (ref Bootconf, string);
+
+		release:	fn(l: self ref Lease);
+	};
+
+	init:	fn();
+	tracing:	fn(debug: int);
+	bootp:	fn(net: string, ctlifc: ref Sys->FD, device: string, init: ref Bootconf): (ref Bootconf, string);
+	dhcp:	fn(net: string, ctlifc: ref Sys->FD, device: string, init: ref Bootconf, options: array of int): (ref Bootconf, ref Lease, string);
+
+	applycfg:	fn(net: string, ctlifc: ref Sys->FD, conf: ref Bootconf): string;
+	removecfg:	fn(net: string, ctlifc: ref Sys->FD, conf: ref Bootconf): string;
+
+	# bootp options used here
+	Opad: con 0;
+	Oend: con 255;
+	Omask: con 1;
+	Orouter: con 3;
+	Odnsserver: con 6;
+	Ocookieserver: con 8;
+	Ohostname: con 12;
+	Odomainname: con 15;
+	Ontpserver: con 42;
+	Ovendorinfo: con 43;
+	Onetbiosns: con 44;
+	Osmtpserver: con 69;
+	Opop3server: con 70;
+	Owwwserver: con 72;
+
+	# dhcp options
+	Oipaddr: con 50;
+	Olease: con 51;
+	Ooverload: con 52;
+	Otype: con 53;
+	Oserverid: con 54;
+	Oparams: con 55;
+	Omessage: con 56;
+	Omaxmsg: con 57;
+	Orenewaltime: con 58;
+	Orebindingtime: con 59;
+	Ovendorclass: con 60;
+	Oclientid: con 61;
+	Otftpserver: con 66;
+	Obootfile: con 67;
+
+	Ovendor:	con (1<<8);
+	OP9fs: con Ovendor|128;	# plan 9 file server
+	OP9auth:	con Ovendor|129;	# plan 9 auth server
+
+	Infinite:	con ~0;	# lease
+};
--- a/module/dhcpd.m	Sat Aug 14 10:59:42 2021
+++ /dev/null	Mon Oct 18 12:05:45 2021
@@ -1,72 +0,0 @@
-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/dhcpserver.m	Sun Aug 15 08:19:51 2021
@@ -0,0 +1,72 @@
+Dhcpserver: module
+{
+	PATH:	con "/dis/lib/dhcpserver.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;
+	};
+};