/*
 * 9boot - load next kernel via pxe (bootp, tftp) and start it
 *
 * intel says that pxe can only load into the bottom 640K,
 * and intel's boot agent takes 128K, leaving only 512K for 9boot.
 *
 * some of this code is from the old 9load's bootp.c.
 */
#include	"u.h"
#include	"../port/lib.h"
#include	"mem.h"
#include	"dat.h"
#include	"fns.h"
#include	"io.h"
#include	"ureg.h"
#include	"pool.h"
#include	"../port/netif.h"
#include	"etherif.h"
#include	"../ip/ip.h"
#include	"pxe.h"

#define TFTPDEF "135.104.9.6"	/* IP of default tftp server */

enum {
	Tftpusehdrs =	0,	/* flag: use announce+headers for tftp? */
	Debug =		0,

	Tftphdrsz =	4,
	/*
	 * this can be bigger than the ether mtu and
	 * will work due to ip fragmentation, at least on v4.
	 */
	Prefsegsize =	1400,
	Maxsegsize =	2048,
	Bufsz =		Maxsegsize + 2,

	Ok =		0,
	Err =		-1,
	Nonexist =	-2,
};

typedef struct Ethaddr Ethaddr;
typedef struct Kernname Kernname;
typedef struct Openeth Openeth;
typedef struct Tftp Tftp;

struct Tftp {
	uchar	header[Tftphdrsz];
	uchar	data[Maxsegsize];
};

struct Kernname {
	char	*edev;
	char	*bootfile;
};

struct Openeth {
	/* names */
	int	ctlrno;
	char	ethname[16];	/* ether%d */
	char	netethname[32];	/* /net/ether%d */
	char	filename[128];	/* from bootp, for tftp */

	Chan	*ifcctl;	/* /net/ipifc/clone */
	Chan	*ethctl;	/* /net/etherN/0/ctl, for promiscuous mode */

	/* udp connection */
	Chan	*udpctl;
	Chan	*udpdata;
	Pxenetaddr *netaddr;
	int	rxactive;
};

struct Ethaddr {		/* communication with sleep procs */
	Openeth	*oe;
	Pxenetaddr *a;
};

static char ethernm[] = "ether";
static uchar myea[Eaddrlen];
static Pxenetaddr myaddr;		/* actually, local ip addr & port */

/*
 * there can be at most one concurrent tftp session until we move these
 * variables into Openeth or some other struct (Tftpstate).
 */
static ushort tftpport;
static int tftpblockno;
static int tftpphase;
static int progress;
static int segsize;
static Tftp *tftpb;
static Pxenetaddr tftpserv;		/* actually, remote ip addr & port */
static Pxenetaddr bootpserv;

static int	tftpconnect(Openeth *, Bootp *);

uchar *
etheraddr(Openeth *oe)
{
	int n;
	char name[32], buf[32];
	static uchar ea[Eaddrlen];

	memset(ea, 0, sizeof ea);
	snprint(name, sizeof name, "#l%d/ether%d/addr", oe->ctlrno, oe->ctlrno);
	n = readfile(name, buf, sizeof buf - 1);
	if (n < 0)
		return ea;
	buf[n] = '\0';
	parseether(ea, buf);
	return ea;
}

static void
udpsend(Openeth *oe, Pxenetaddr *a, void *data, int dlen)
{
	int n;
	uchar *buf;
	Chan *c;
	Etherpkt pkt;
	Udphdr *uh;

	buf = data;
	if (dlen > sizeof pkt)
		panic("udpsend: packet too big");

	oe->netaddr = a;
	/*
	 * add Plan 9 UDP pseudo-headers
	 */
	if (!tftpphase || Tftpusehdrs) {
		memset(&pkt, 0, sizeof pkt);
		uh = (Udphdr*)&pkt;
		memmove(uh + 1, data, dlen);
		USED(buf);
		buf = (uchar *)uh;
		dlen += sizeof *uh;
		if (dlen > sizeof pkt)
			panic("udpsend: packet too big");

		ipmove(uh->laddr, myaddr.ip);
		hnputs(uh->lport, myaddr.port);
		ipmove(uh->raddr, a->ip);
		hnputs(uh->rport, a->port);
		if(Debug)
			print("udpsend %I!%d -> %I!%d ", uh->laddr,
				nhgets(uh->lport), uh->raddr, nhgets(uh->rport));
	}
	if (waserror()) {
		iprint("udp write error\n");
		return;			/* send another req later */
	}
	c = oe->udpdata;
	assert(oe->udpdata != nil);
	n = devtab[c->type]->write(c, buf, dlen, c->offset);
	poperror();
	c->offset += n;
	if (n != dlen)
		print("udpsend: wrote %d/%d\n", n, dlen);
	else if (progress)
		print(".");
}

static void
nak(Openeth *oe, Pxenetaddr *a, int code, char *msg, int report)
{
	char buf[4 + 32];

	buf[0] = 0;
	buf[1] = Tftp_ERROR;
	buf[2] = 0;
	buf[3] = code;
	strncpy(buf+4, msg, sizeof buf - 4 - 1);
	udpsend(oe, a, buf, 4 + strlen(buf+4) + 1);
	if(report)
		print("\ntftp: error(%d): %s\n", code, msg);
}

/* a is the source address we're looking for */
static int
tuplematch(Pxenetaddr *a, Udphdr *h)
{
	int port;
	uchar *ip;

	if (tftpphase && !Tftpusehdrs)
		return 1;
	/*
	 * we're using udp headers mode, because we're still doing bootp,
	 * or we are doing tftp and we chose to use headers mode.
	 */
	port = a->port;
	ip = a->ip;
	/*
	 * we're accepting any src port or it's from the port we want, and
	 * it's from the ip we want or we sent to a broadcast address, and
	 * it's for us or it's a broadcast.
	 */
	return (port == 0 || nhgets(h->rport) == port) &&
		(equivip6(h->raddr, ip) || equivip6(ip, IPv4bcast)) &&
		(equivip6(h->laddr, myaddr.ip) || equivip6(h->laddr, IPv4bcast));
}

/* extract UDP payload into data and set a */
static int
udppayload(Udphdr *h, int len, Pxenetaddr *a, uchar *data, int dlen)
{
	if(Debug)
		print("udprecv %I!%d to %I!%d...\n",
			h->raddr, nhgets(h->rport), h->laddr, nhgets(h->lport));

	if(a->port != 0 && nhgets(h->rport) != a->port) {
		if(Debug)
			print("udpport %ux not %ux\n", nhgets(h->rport), a->port);
		return -1;
	}

	if(!equivip6(a->ip, IPv4bcast) && !equivip6(a->ip, h->raddr)) {
		if(Debug)
			print("bad ip %I not %I\n", h->raddr, a->ip);
		return -1;
	}

	len -= sizeof *h;		/* don't count pseudo-headers */
	if(len > dlen) {
		print("udp packet too big: %d > %d; from addr %I\n",
			len, dlen, h->raddr);
		return -1;
	}
	memmove(data, h + 1, len);	/* skip pseudo-headers */

	/* set a from remote address */
	ipmove(a->ip, h->raddr);
	a->port = nhgets(h->rport);
	return len;
}

static int
chanlen(Chan *ch)
{
	int len;
	Dir *dp;

	dp = dirchstat(ch);
	if (dp == nil)
		return -1;
	len = dp->length;		/* qlen(cv->rq) in devip */
	free(dp);
	return len;
}

static int
udprecv(Openeth *oe, Pxenetaddr *a, void *data, int dlen)
{
	int len, buflen, chlen;
	ulong timo, now;
	char *buf;
	Chan *c;
	Etherpkt pkt;

	oe->netaddr = a;
	/* timo is frequency of tftp ack and broadcast bootp retransmission */
	if(oe->rxactive == 0)
		timo = 1000;
	else
		timo = Timeout;
	now = TK2MS(m->ticks);
	timo += now;			/* deadline */

	c = oe->udpdata;
	spllo();			/* paranoia */
	do {
		/*
		 * wait for data to arrive or time-out.
		 * alarms only work for user procs, so we poll to avoid getting
		 * stuck in ipread.
		 */
		for (chlen = chanlen(c); chlen == 0 && now < timo;
		     chlen = chanlen(c)) {
			/* briefly give somebody else a chance to run */
			tsleep(&up->sleep, return0, 0, 0);
			now = TK2MS(m->ticks);
		}
		if (chlen <= 0) {
			print("T");
			return -1;		/* timed out */
		}

		while (waserror()) {
			print("read err: %s\n", up->errstr);
			tsleep(&up->sleep, return0, 0, 1000);
		}

		/*
		 * using Plan 9 UDP pseudo-headers?
		 */
		if (tftpphase && !Tftpusehdrs) {
			buf = data;	/* read directly in caller's buffer */
			buflen = dlen;
		} else {
			buf = (char *)&pkt;  /* read pkt with hdrs */
			buflen = sizeof pkt;
		}
		/* devtab[c->type]->read calls ipread */
		len = devtab[c->type]->read(c, buf, buflen, c->offset);
		poperror();

		if (len <= 0)
			return len;
		c->offset += len;
	} while (!tuplematch(oe->netaddr, (Udphdr *)buf));

	/*
	 * using Plan 9 UDP pseudo-headers? extract payload into caller's buf.
	 */
	if (!tftpphase || Tftpusehdrs)
		len = udppayload((Udphdr *)&pkt, len, a, data, dlen);
	if (len >= 0)
		oe->rxactive = 1;
	return len;
}

static void
ack(Openeth *oe, Pxenetaddr *a, int blkno)
{
	char buf[4];

	buf[0] = 0;
	buf[1] = Tftp_ACK;
	buf[2] = blkno>>8;
	buf[3] = blkno;
	udpsend(oe, a, buf, sizeof buf);
}

static char *
skipwd(char *wd)
{
	while (*wd != '\0')
		wd++;
	return wd + 1;		/* skip terminating NUL */
}

static int
optval(char *opt, char *pkt, int len)
{
	char *wd, *ep, *p;

	ep = pkt + len;
	for (p = pkt; p < ep && *p != '\0'; p = skipwd(wd)) {
		wd = skipwd(p);
		if (cistrcmp(p, opt) == 0)
			return strtol(wd, 0, 10);
	}
	return -1;
}

/*
 * send a tftp read request to `a' for name.  if we get a data packet back,
 * ack it and stash it in tftp for later.
 *
 * format of a request packet, from the RFC:
 *
 *          2 bytes     string    1 byte     string   1 byte
 *          ------------------------------------------------
 *         | Opcode |  Filename  |   0  |    Mode    |   0  |
 *          ------------------------------------------------
 */
static int
tftpread1st(Openeth *oe, Pxenetaddr *a, char *name, Tftp *tftp)
{
	int i, n, len, rlen, oport, sendack;
	static char *buf;

	if (buf == nil)
		buf = malloc(Bufsz);
	buf[0] = 0;
	buf[1] = Tftp_READ;
	len = 2 + snprint(buf+2, Bufsz - 2, "%s", name) + 1;
	len += snprint(buf+len, Bufsz - len, "octet") + 1;
	len += snprint(buf+len, Bufsz - len, "blksize") + 1; /* option */
	len += snprint(buf+len, Bufsz - len, "%d", Prefsegsize) + 1;

	/*
	 * keep sending the same packet until we get an answer.
	 */
	if (Debug)
		print("tftpread1st %s\n", name);
	oe->netaddr = a;
	/*
	 * the first packet or two sent seem to get dropped,
	 * so use a shorter time-out on the first packet.
	 */
	oe->rxactive = 0;
	oport = a->port;
	tftpblockno = 0;
	segsize = Defsegsize;
	sendack = 0;
	for(i = 0; i < 10; i++){
		a->port = oport;
		if (sendack)
			ack(oe, a, tftpblockno);
		else
			udpsend(oe, a, buf, len);	/* tftp read name */

		if((rlen = udprecv(oe, a, tftp, sizeof(Tftp))) < Tftphdrsz)
			continue;		/* runt or time-out */

		switch((tftp->header[0]<<8)|tftp->header[1]){

		case Tftp_ERROR:
			if(strstr((char *)tftp->data, "does not exist") != nil){
				print("%s\n", (char*)tftp->data);
				return Nonexist;
			}
			print("tftpread1st: error (%d): %s\n",
				(tftp->header[2]<<8)|tftp->header[3], (char*)tftp->data);
			return Err;

		case Tftp_OACK:
			n = optval("blksize", (char *)tftp->header+2, rlen-2);
			if (n <= 0) {
				nak(oe, a, 0, "bad blksize option value", 0);
				return Err;
			}
			segsize = n;
			/* no bytes stashed in tftp.data */
			i = 0;
			sendack = 1;
			break;

		case Tftp_DATA:
			tftpblockno = 1;
			len = (tftp->header[2]<<8)|tftp->header[3];
			if(len != tftpblockno){
				print("tftpread1st: block error: %d\n", len);
				nak(oe, a, 1, "block error", 0);
				return Err;
			}
			rlen -= Tftphdrsz;
			if(rlen < segsize)
				/* ACK now, in case we don't later */
				ack(oe, a, tftpblockno);
			return rlen;

		default:
			print("tftpread1st: unexpected pkt type recv'd\n");
			nak(oe, a, 0, "unexpected pkt type recv'd", 0);
			return Err;
		}
	}

	print("tftpread1st: failed to connect to server (%I!%d)\n", a->ip, oport);
	return Err;
}

static int
tftpread(Openeth *oe, Pxenetaddr *a, Tftp *tftp, int dlen)
{
	int try, blockno, len;

	dlen += Tftphdrsz;

	/*
	 * keep sending ACKs until we get an answer.
	 */
	for(try = 0; try < 10; try++) {
		ack(oe, a, tftpblockno);

		len = udprecv(oe, a, tftp, dlen);
		/*
		 * NB: not `<='; just a header is legal and happens when
		 * file being read is a multiple of segsize bytes long.
		 */
		if(len < Tftphdrsz){
			if(Debug)
				print("tftpread: too short %d <= %d\n",
					len, Tftphdrsz);
			continue;
		}
		switch((tftp->header[0]<<8)|tftp->header[1]){
		case Tftp_ERROR:
			print("tftpread: error (blk %d): %s\n",
				(tftp->header[2]<<8)|tftp->header[3],
				(char*)tftp->data);
			nak(oe, a, 0, "error pkt recv'd", 0);
			return -1;
		case Tftp_OACK:
			print("tftpread: oack pkt recv'd too late\n");
			nak(oe, a, 0, "oack pkt recv'd too late", 0);
			return -1;
		default:
			print("tftpread: unexpected pkt type recv'd\n");
			nak(oe, a, 0, "unexpected pkt type recv'd", 0);
			return -1;
		case Tftp_DATA:
			break;
		}
		blockno = (tftp->header[2]<<8)|tftp->header[3];
		if(blockno <= tftpblockno){
			if(Debug)
				print("tftpread: blkno %d <= %d\n",
					blockno, tftpblockno);
			continue;
		}

		if(blockno == tftpblockno+1) {
			tftpblockno++;
			if(len < dlen)	/* last packet? send final ack */
				ack(oe, a, tftpblockno);
			return len-Tftphdrsz;
		}
		print("tftpread: block error: %d, expected %d\n",
			blockno, tftpblockno+1);
	}

	return -1;
}

/*
 * broadcast a bootp request for file.  stash any answer in rep.
 */
static int
bootpbcast(Openeth *oe, char *file, Bootp *rep)
{
	Bootp req;
	int i;
	uchar *ea;
	char name[128], *filename, *sysname;
	static char zeroes[IPaddrlen];

	oe->filename[0] = '\0';
	if (Debug)
		if (file == nil)
			print("bootpopen: %s...", oe->ethname);
		else
			print("bootpopen: %s!%s...", oe->ethname, file);
	if((ea = etheraddr(oe)) == nil){
		print("bad ether %s\n", oe->ethname);
		return -1;
	}

	filename = nil;
	sysname = 0;
	if(file && *file){
		strncpy(name, file, sizeof name);
		if(filename = strchr(name, '!')){
			sysname = name;
			*filename++ = 0;
		}
		else
			filename = name;
	}

	/*
	 * form a bootp request packet
	 */
	memset(&req, 0, sizeof(req));
	req.op = Bootrequest;
	req.htype = 1;			/* ethernet */
	req.hlen = Eaddrlen;		/* ethernet */
	memmove(req.chaddr, ea, Eaddrlen);
	req.flags[0] = 0x80;		/* request broadcast reply */
	if(filename != nil) {
		strncpy(req.file, filename, sizeof(req.file));
		strncpy(oe->filename, filename, sizeof oe->filename);
	}
	if(sysname != nil)		/* if server name given, supply it */
		strncpy(req.sname, sysname, sizeof(req.sname));

	if (memcmp(myaddr.ip, zeroes, sizeof myaddr.ip) == 0)
		ipmove(myaddr.ip, IPv4bcast);	/* didn't know my ip yet */
	myaddr.port = BPportsrc;
	memmove(myea, ea, Eaddrlen);

	/* send to 255.255.255.255!67 */
	ipmove(bootpserv.ip, IPv4bcast);
	bootpserv.port = BPportdst;

	/*
	 * send it until we get a matching answer
	 */
	memset(rep, 0, sizeof *rep);
	for(i = 10; i > 0; i--) {
		req.xid[0] = i;			/* try different xids */
		udpsend(oe, &bootpserv, &req, sizeof(req));

		if(udprecv(oe, &bootpserv, rep, sizeof(*rep)) <= 0)
			continue;
		if(memcmp(req.chaddr, rep->chaddr, Eaddrlen) != 0)
			continue;
		if(rep->htype != 1 || rep->hlen != Eaddrlen)
			continue;
		if(sysname == 0 || strcmp(sysname, rep->sname) == 0)
			break;
	}
	if(i <= 0) {
		if (file == nil)
			print("bootp on %s timed out\n", oe->ethname);
		else
			print("bootp on %s for %s timed out\n", oe->ethname, file);
		return -1;
	}
	return 0;
}

/*
 * request file via tftp from server named in rep.
 * initial data packet will be stashed in tftpb.
 */
static int
tftpopen(Openeth *oe, char *file, Bootp *rep)
{
	char *filename;
	char buf[128];
	static uchar ipv4noaddr[IPv4addrlen];

	if (tftpconnect(oe, rep) < 0)
		return Err;

	/*
	 * read file from tftp server in bootp answer
	 */
	filename = oe->filename;
	if (file)
		filename = file;
	if(filename == 0 || *filename == 0){
		if(strcmp(rep->file, "/386/9boot") == 0 ||
		   strcmp(rep->file, "/386/9pxeload") == 0) {
			print("won't load another boot loader (%s)\n", rep->file);
			return -1;		/* avoid infinite loop */
		}
		filename = rep->file;
	}

	print("\n");
	if(rep->sname[0] != '\0')
		print("%s ", rep->sname);

	v4tov6(myaddr.ip, rep->yiaddr);
	myaddr.port = tftpport;
	if (equivip4(rep->siaddr, ipv4noaddr)) { /* no server address? */
		getstr("tftp server IP address", buf, sizeof buf, TFTPDEF, 0);
		v4parseip(rep->siaddr, buf);
	}
	v4tov6(tftpserv.ip, rep->siaddr);
	tftpserv.port = TFTPport;
	if (tftpb == nil)
		tftpb = malloc(sizeof *tftpb);

	print("(%V!%d): %s ", rep->siaddr, tftpserv.port, filename);

	return tftpread1st(oe, &tftpserv, filename, tftpb);
}

/* load the kernel in file via tftp on oe */
int
tftpboot(Openeth *oe, char *file, Bootp *rep, Boot *b)
{
	int n;

	/* file must exist, else it's an error */
	if((n = tftpopen(oe, file, rep)) < 0)
		return n;

	progress = 0;			/* no more dots; we're on a roll now */
	print(" ");			/* after "sys (ip!port): kernel ..." */
	while(bootpass(b, tftpb->data, n) == MORE){
		n = tftpread(oe, &tftpserv, tftpb, segsize);
		if(n < segsize)
			break;
	}
	if(0 < n && n < segsize)	/* got to end of file */
		bootpass(b, tftpb->data, n);
	else
		nak(oe, &tftpserv, 3, "ok", 0);	/* tftpclose to abort transfer */
	bootpass(b, nil, 0);	/* boot if possible */
	return Err;
}

/* leave the channel to /net/ipifc/clone open */
static int
binddevip(Openeth *oe)
{
	Chan *icc;
	char buf[32];

	if (waserror()) {
		print("binddevip: can't bind ether %s: %s\n",
			oe->netethname, up->errstr);
		nexterror();
	}
	/* get a new ip interface */
	oe->ifcctl = icc = namecopen("/net/ipifc/clone", ORDWR);
	if(icc == nil)
		error("can't open /net/ipifc/clone");

	/*
	 * specify medium as ethernet, bind the interface to it.
	 * this should trigger chandial of types 0x800, 0x806 and 0x86dd.
	 */
	snprint(buf, sizeof buf, "bind ether %s", oe->netethname);
	devtab[icc->type]->write(icc, buf, strlen(buf), 0);  /* bind ether %s */
	poperror();
	return 0;
}

/* set the default route */
static int
adddefroute(char *, uchar *gaddr)
{
	char buf[64];
	Chan *rc;

	rc = nil;
	if (waserror()) {
		if (rc)
			cclose(rc);
		return -1;
	}
	rc = enamecopen("/net/iproute", ORDWR);

	if(isv4(gaddr))
		snprint(buf, sizeof buf, "add 0 0 %I", gaddr);
	else
		snprint(buf, sizeof buf, "add :: /0 %I", gaddr);
	devtab[rc->type]->write(rc, buf, strlen(buf), 0);
	poperror();
	cclose(rc);
	return 0;
}

static int
validip(uchar *ip)
{
	return ipcmp(ip, IPnoaddr) != 0 && ipcmp(ip, v4prefix) != 0;
}

static int
openetherdev(Openeth *oe)
{
	int n;
	char num[16];
	Chan *c;
	static char promisc[] = "promiscuous";

	if (chdir(oe->netethname) < 0)
		return -1;			/* out of ethers */

	oe->ethctl = nil;
	if (waserror()) {
		print("error opening /net/ether%d/0/ctl: %s\n",
			oe->ctlrno, up->errstr);
		if (oe->ethctl) {
			cclose(oe->ethctl);
			oe->ethctl = nil;
		}
		chdir("/");			/* don't hold conv. open */
		return -1;
	}
	oe->ethctl = c = namecopen("0/ctl", ORDWR);	/* should be ipv4 */
	if (c == nil) {
		/* read clone file to make conversation 0 since not present */
		oe->ethctl = c = enamecopen("clone", ORDWR);
		n = devtab[c->type]->read(c, num, sizeof num - 1, 0);
		if (n < 0)
			print("no %s/clone: %s\n", oe->netethname, up->errstr);
		else {
			num[n] = 0;
			print("%s/clone returned %s\n", oe->netethname, num);
		}
	}
	/* shouldn't be needed to read bootp (broadcast) reply */
	devtab[c->type]->write(c, promisc, sizeof promisc-1, 0);
	poperror();
	chdir("/");
	/* leave oe->ethctl open to keep promiscuous mode on */
	return 0;
}

/* add a logical interface to the ip stack */
int
minip4cfg(Openeth *oe)
{
	int n;
	char buf[64];

	n = snprint(buf, sizeof buf, "add %I", IPnoaddr);
	devtab[oe->ifcctl->type]->write(oe->ifcctl, buf, n, 0);	/* add %I */

	openetherdev(oe);
	return 0;
}

/* remove the :: address added by minip4cfg */
int
unminip4cfg(Openeth *oe)
{
	int n;
	char buf[64];

	n = snprint(buf, sizeof buf, "remove %I /128", IPnoaddr);
	if (waserror()) {
		print("failed write to ifc: %s: %s\n", buf, up->errstr);
		return -1;
	}
	devtab[oe->ifcctl->type]->write(oe->ifcctl, buf, n, 0);	/* remove %I */
	cclose(oe->ethctl);		/* turn promiscuous mode off */
	oe->ethctl = nil;
	poperror();
	return 0;
}

/*
 * parse p, looking for option `op'.  if non-nil, np points to minimum length.
 * return nil if option is too small, else ptr to opt, and
 * store actual length via np if non-nil.
 */
uchar*
optget(uchar *p, int op, int *np)
{
	int len, code;

	while ((code = *p++) != OBend) {
		if(code == OBpad)
			continue;
		len = *p++;
		if(code != op) {
			p += len;
			continue;
		}
		if(np != nil){
			if(*np > len)
				return 0;
			*np = len;
		}
		return p;
	}
	return 0;
}

int
optgetaddr(uchar *p, int op, uchar *ip)
{
	int len;

	len = 4;
	p = optget(p, op, &len);
	if(p == nil)
		return 0;
	v4tov6(ip, p);
	return 1;
}

int beprimary = 1;

/* add a logical interface to the ip stack */
int
ip4cfg(Openeth *oe, Bootp *rep)
{
	int n;
	uchar gaddr[IPaddrlen], v6mask[IPaddrlen];
	uchar v4mask[IPv4addrlen];
	char buf[64];
	static uchar zeroes[4];

	v4tov6(gaddr, rep->yiaddr);
	if(!validip(gaddr))
		return -1;

	/* dig subnet mask, if any, out of options.  if none, guess. */
	if(optgetaddr(rep->optdata, OBmask, v6mask)) {
		v6tov4(v4mask, v6mask);
		n = snprint(buf, sizeof buf, "add %V %M", rep->yiaddr, v4mask);
	} else
		n = snprint(buf, sizeof buf, "add %V 255.255.255.0", rep->yiaddr);

	devtab[oe->ifcctl->type]->write(oe->ifcctl, buf, n, 0);

	v4tov6(gaddr, rep->giaddr);
	if(beprimary==1 && validip(gaddr) && !equivip4(rep->giaddr, zeroes))
		adddefroute("/net", gaddr);
	return 0;
}

static int
openudp(Openeth *oe)
{
	int n;
	char buf[16];
	Chan *cc;

	/* read clone file for conversation number */
	if (waserror())
		panic("openudp: can't open /net/udp/clone");
	cc = enamecopen("/net/udp/clone", ORDWR);
	oe->udpctl = cc;
	n = devtab[cc->type]->read(cc, buf, sizeof buf - 1, 0);
	poperror();
	buf[n] = '\0';
	return atoi(buf);
}

static void
initbind(Openeth *oe)
{
	char buf[8];

	if (waserror()) {
		print("error while binding: %s\n", up->errstr);
		return;
	}
	snprint(buf, sizeof buf, "#I%d", oe->ctlrno);
	bind(buf, "/net", MAFTER);
	snprint(buf, sizeof buf, "#l%d", oe->ctlrno);
	bind(buf, "/net", MAFTER);
	binddevip(oe);
	poperror();
}

static void
closeudp(Openeth *oe)
{
	if (oe->udpctl) {
		cclose(oe->udpctl);
		oe->udpctl = nil;
	}
	if (oe->udpdata) {
		cclose(oe->udpdata);
		oe->udpdata = nil;
	}
}

static int
announce(Openeth *oe, char *port)
{
	int udpconv;
	char buf[32];
	static char hdrs[] = "headers";

	while (waserror()) {
		print("can't announce udp!*!%s: %s\n", port, up->errstr);
		closeudp(oe);
		nexterror();
	}
	udpconv = openudp(oe);
	if (udpconv < 0)
		panic("can't open udp conversation: %s", up->errstr);

	/* headers is only effective after a udp announce */
	snprint(buf, sizeof buf, "announce %s", port);
	devtab[oe->udpctl->type]->write(oe->udpctl, buf, strlen(buf), 0);
	devtab[oe->udpctl->type]->write(oe->udpctl, hdrs, sizeof hdrs - 1, 0);
	poperror();

	/* now okay to open the data file */
	snprint(buf, sizeof buf, "/net/udp/%d/data", udpconv);
	/*
	 * we must use create, not open, to get Conv->rq and ->wq
	 * allocated by udpcreate.
	 */
	oe->udpdata = enameccreate(buf, ORDWR);
	cclose(oe->udpctl);
	oe->udpctl = nil;
	return udpconv;
}

static long
tftprdfile(Openeth *oe, int openread, void* va, long len)
{
	int n;
	char *p, *v;

	n = openread;	/* have read this many bytes already into tftpb->data */
	p = v = va;
	len--;				/* leave room for NUL */
	while(n > 0) {
		if((p-v)+n > len)
			n = len - (p-v);
		memmove(p, tftpb->data, n);
		p += n;
		*p = 0;
		if(n != segsize)
			break;

		if((n = tftpread(oe, &tftpserv, tftpb, segsize)) < 0)
			return n;
	}
	return p-v;
}

static int
tftpconnect(Openeth *oe, Bootp *rep)
{
	char num[16], dialstr[64];

	if (waserror()) {
		print("can't dial: %s\n", up->errstr);
		return -1;
	}
	closeudp(oe);

	tftpphase = 1;
	tftpport = 5000 + nrand(20480);
	snprint(num, sizeof num, "%d", tftpport);
	if (Tftpusehdrs)
		announce(oe, num);
	else {
		snprint(dialstr, sizeof dialstr, "/net/udp!%V!%d",
			rep->siaddr, TFTPport);
		oe->udpdata = chandial(dialstr, num, nil, nil);
		oe->udpctl = nil;
	}
	poperror();
	return 0;
}

static int
setipcfg(Openeth *oe, Bootp *rep)
{
	int r;

	tftpphase = 0;
	progress = 1;

	/* /net/iproute is unpopulated here; add at least broadcast */
	minip4cfg(oe);
	announce(oe, "68");
	r = bootpbcast(oe, nil, rep);
	closeudp(oe);
	unminip4cfg(oe);
	if(r < 0)
		return -1;

	ip4cfg(oe, rep);
	if (Debug)
		print("got & set ip config\n");
	return 0;
}

/*
 * use bootp answer (rep) to open cfgpxe.
 * reads first pkt of cfgpxe into tftpb->data.
 */
static int
rdcfgpxe(Openeth *oe, Bootp *rep, char *cfgpxe)
{
	int n;
	char *ini;

	/* cfgpxe is optional */
	n = tftpopen(oe, cfgpxe, rep);
	if (n < 0)
		return n;
	if (Debug)
		print("\opened %s\n", cfgpxe);

	ini = smalloc(2*BOOTARGSLEN);
	/* starts by copying data from tftpb->data into ini */
	n = tftprdfile(oe, n, ini, 2*BOOTARGSLEN);
	if (n < 0) {
		print("error reading %s\n", cfgpxe);
		free(ini);
		return n;
	}
	print(" read %d bytes", n);

	/*
	 * take note of plan9.ini contents.  consumes ini to make config vars,
	 * thus we can't free ini.
	 */
	dotini(ini);
	return Ok;
}

/*
 * break kp->bootfile into kp->edev & kp->bootfile,
 * copy any args for new kernel to low memory.
 */
static int
parsebootfile(Kernname *kp)
{
	char *p;

	p = strchr(kp->bootfile, '!');
	if (p != nil) {
		*p++ = '\0';
		kp->edev = kp->bootfile;
		kp->bootfile = nil;
		kstrdup(&kp->bootfile, p);
		if (strncmp(kp->edev, ethernm, sizeof ethernm - 1) != 0) {
			print("bad ether device %s\n", kp->edev);
			return Err;
		}
	}

	/* pass any arguments to kernels that expect them */
	strecpy(BOOTLINE, BOOTLINE+BOOTLINELEN, kp->bootfile);
	p = strchr(kp->bootfile, ' ');
	if(p != nil)
		*p = '\0';
	return Ok;
}

static int
getkernname(Openeth *oe, Bootp *rep, Kernname *kp)
{
	int n;
	char *p;
	char cfgpxe[32], buf[64];

	if (kp->bootfile) {
		/* i think returning here is a bad idea */
		// print("getkernname: already have bootfile %s\n",
		//	kp->bootfile);
		free(kp->bootfile);
		// return Ok;
	}
	kp->edev = kp->bootfile = nil;
	i8250console();		/* configure serial port with defaults */

	/* use our mac address instead of relying on a bootp answer. */
	snprint(cfgpxe, sizeof cfgpxe, "/cfg/pxe/%E", myea);
	n = rdcfgpxe(oe, rep, cfgpxe);
	switch (n) {
	case Ok:
		p = getconf("bootfile");
		if (p)
			kstrdup(&kp->bootfile, p);
		if (kp->bootfile == nil)
			askbootfile(buf, sizeof buf, &kp->bootfile, Promptsecs,
				"ether0!/386/9pccpu");
		if (strcmp(kp->bootfile, "manual") == 0)
			askbootfile(buf, sizeof buf, &kp->bootfile, 0, "");
		break;
	case Err:
		print("\nfailed.\n");
		return n;
	case Nonexist:
		askbootfile(buf, sizeof buf, &kp->bootfile, 0, "");
		break;
	}
	return parsebootfile(kp);
}

static void
unbinddevip(Openeth *oe)
{
	Chan *icc;
	static char unbind[] = "unbind";

	icc = oe->ifcctl;
	if (icc) {
		devtab[icc->type]->write(icc, unbind, sizeof unbind - 1, 0);
		cclose(icc);
		oe->ifcctl = nil;
	}
}

/*
 * phase 1: get our ip (v4) configuration via bootp, set new ip configuration.
 * phase 2: load /cfg/pxe, parse it, extract kernel filename.
 * phase 3: load kernel and jump to it.
 */
static int
tftpload(Openeth *oe, Kernname *kp)
{
	int r, n;
	char buf[64];
	Bootp rep;
	Boot boot;

	r = -1;
	if(waserror()) {
		print("tftpload: %s\n", up->errstr);
		closeudp(oe);
		unbinddevip(oe);
		return r;
	}

	memset(&rep, 0, sizeof rep);
	if (setipcfg(oe, &rep) < 0)
		error("can't set ip config");

	n = getkernname(oe, &rep, kp);
	if (n < 0) {
		r = n;			/* pass reason back to caller */
		USED(r);
		nexterror();
	}
	do {
		if (kp->edev &&
		    oe->ctlrno != strtol(kp->edev + sizeof ethernm - 1, 0, 10)){
			/* user specified an ether & it's not this one; next! */
			r = Ok;
			USED(r);
			nexterror();
		}

		memset(&boot, 0, sizeof boot);
		boot.state = INITKERNEL;
		r = tftpboot(oe, kp->bootfile, &rep, &boot);

		/* we failed or bootfile asked for another ether */
		if (r == Nonexist)
			do {
				askbootfile(buf, sizeof buf, &kp->bootfile, 0, "");
			} while (parsebootfile(kp) != Ok);
	} while (r == Nonexist);

	poperror();
	closeudp(oe);
	unbinddevip(oe);
	return r;
}

static int
etherload(int eth, Kernname *kp)
{
	int r;
	Openeth *oe;

	print("pxe on ether%d ", eth);
	oe = smalloc(sizeof *oe);
	memset(oe, 0, sizeof *oe);
	oe->ctlrno = eth;
	snprint(oe->ethname, sizeof oe->ethname, "ether%d", oe->ctlrno);
	snprint(oe->netethname, sizeof oe->netethname, "/net/ether%d",
		oe->ctlrno);
	initbind(oe);

	r = tftpload(oe, kp);

	/* failed to boot; keep going */
	unmount(nil, "/net");
	return r;
}

static int
attacheth(int neth)
{
	char num[4];
	Chan *cc;

	cc = nil;
	if (waserror()) {		/* no more interfaces */
		if (cc)
			cclose(cc);
		return -1;
	}
	snprint(num, sizeof num, "%d", neth);
	cc = etherattach(num);
	if (cc)
		cclose(cc);
	poperror();
	return cc == nil? -1: 0;
}

void
bootloadproc(void *)
{
	int eth, neth, needattach;
	Kernname kernnm;

	srand(TK2MS(m->ticks));			/* for local port numbers */
	nrand(20480);				/* 1st # is always 0; toss it */
	kernnm.edev = kernnm.bootfile = nil;

	while(waserror()) {
		print("%s\n", up->errstr);
		tsleep(&up->sleep, return0, 0, 30*1000);
	}
	neth = MaxEther;
	needattach = 1;
	for (;;) {
		/* try each interface in turn: first get /cfg/pxe file */
		for (eth = 0; eth < neth && kernnm.edev == nil; eth++) {
			if (needattach && attacheth(eth) < 0)
				break;
			etherload(eth, &kernnm);
		}
		if (needattach) {
			neth = eth;
			needattach = 0;
			if (neth == 0)
				print("no ethernet interfaces found\n");
		}
		if (kernnm.edev != nil) {
			eth = strtol(kernnm.edev + sizeof ethernm - 1, 0, 10);
			etherload(eth, &kernnm);
		}
		/*
		 * couldn't boot on any ether.  don't give up;
		 * perhaps the boot servers are down, so try again later.
		 */
		print("failed to boot via pxe; will try again.\n");
		tsleep(&up->sleep, return0, 0, 15*1000);
	}
}
