// SPDX-License-Identifier: GPL-2.0-or-later

/* PASST - Plug A Simple Socket Transport
 *  for qemu/UNIX domain socket mode
 *
 * PASTA - Pack A Subtle Tap Abstraction
 *  for network namespace/tap device mode
 *
 * dhcp.c - Minimalistic DHCP server for PASST
 *
 * Copyright (c) 2020-2021 Red Hat GmbH
 * Author: Stefano Brivio <sbrivio@redhat.com>
 */

#include <arpa/inet.h>
#include <net/if.h>
#include <netinet/if_ether.h>
#include <netinet/ip.h>
#include <netinet/udp.h>
#include <stddef.h>
#include <stdio.h>
#include <stdint.h>
#include <unistd.h>
#include <string.h>
#include <limits.h>

#include "util.h"
#include "ip.h"
#include "checksum.h"
#include "packet.h"
#include "passt.h"
#include "tap.h"
#include "log.h"
#include "dhcp.h"

/**
 * struct opt - DHCP option
 * @sent:	Convenience flag, set while filling replies
 * @slen:	Length of option defined for server
 * @s:		Option payload from server
 * @clen:	Length of option received from client
 * @c:		Option payload from client
 */
struct opt {
	int sent;
	int slen;
	uint8_t s[255];
	int clen;
	uint8_t c[255];
};

static struct opt opts[255];

#define DHCPDISCOVER	1
#define DHCPOFFER	2
#define DHCPREQUEST	3
#define DHCPDECLINE	4
#define DHCPACK		5
#define DHCPNAK		6
#define DHCPRELEASE	7
#define DHCPINFORM	8
#define DHCPFORCERENEW	9

#define OPT_MIN		60 /* RFC 951 */

/**
 * dhcp_init() - Initialise DHCP options
 */
void dhcp_init(void)
{
	opts[1]  = (struct opt) { 0, 4, {     0 }, 0, { 0 }, };	/* Mask */
	opts[3]  = (struct opt) { 0, 4, {     0 }, 0, { 0 }, };	/* Router */
	opts[51] = (struct opt) { 0, 4, {  0xff,
					   0xff,
					   0xff,
					   0xff }, 0, { 0 }, };	/* Lease time */
	opts[53] = (struct opt) { 0, 1, {     0 }, 0, { 0 }, };	/* Type */
	opts[54] = (struct opt) { 0, 4, {     0 }, 0, { 0 }, };	/* Server ID */
}

/**
 * struct msg - BOOTP/DHCP message
 * @op:		BOOTP message type
 * @htype:	Hardware address type
 * @hlen:	Hardware address length
 * @hops:	DHCP relay hops
 * @xid:	Transaction ID randomly chosen by client
 * @secs:	Seconds elapsed since beginning of acquisition or renewal
 * @flags:	DHCP message flags
 * @ciaddr:	Client IP address in BOUND, RENEW, REBINDING
 * @yiaddr:	IP address being offered or assigned
 * @siaddr:	Next server to use in bootstrap
 * @giaddr:	Relay agent IP address
 * @chaddr:	Client hardware address
 * @sname:	Server host name
 * @file:	Boot file name
 * @magic:	Magic cookie prefix before options
 * @o:		Options
 */
struct msg {
	uint8_t op;
#define BOOTREQUEST	1
#define BOOTREPLY	2
	uint8_t htype;
	uint8_t hlen;
	uint8_t hops;
	uint32_t xid;
	uint16_t secs;
	uint16_t flags;
	uint32_t ciaddr;
	struct in_addr yiaddr;
	uint32_t siaddr;
	uint32_t giaddr;
	uint8_t chaddr[16];
	uint8_t sname[64];
	uint8_t file[128];
	uint32_t magic;
	uint8_t o[308];
} __attribute__((__packed__));

/**
 * fill_one() - Fill a single option in message
 * @m:		Message to fill
 * @o:		Option number
 * @offset:	Current offset within options field, updated on insertion
 */
static void fill_one(struct msg *m, int o, int *offset)
{
	m->o[*offset] = o;
	m->o[*offset + 1] = opts[o].slen;
	memcpy(&m->o[*offset + 2], opts[o].s, opts[o].slen);

	opts[o].sent = 1;
	*offset += 2 + opts[o].slen;
}

/**
 * fill() - Fill options in message
 * @m:		Message to fill
 *
 * Return: current size of options field
 */
static int fill(struct msg *m)
{
	int i, o, offset = 0;

	m->op = BOOTREPLY;
	m->secs = 0;

	for (o = 0; o < 255; o++)
		opts[o].sent = 0;

	/* Some clients (wattcp32, mTCP, maybe some others) expect
	 * option 53 at the beginning of the list.
	 * Put it there explicitly, unless requested via option 55.
	 */
	if (!memchr(opts[55].c, 53, opts[55].clen))
		fill_one(m, 53, &offset);

	for (i = 0; i < opts[55].clen; i++) {
		o = opts[55].c[i];
		if (opts[o].slen)
			fill_one(m, o, &offset);
	}

	for (o = 0; o < 255; o++) {
		if (opts[o].slen && !opts[o].sent)
			fill_one(m, o, &offset);
	}

	m->o[offset++] = 255;
	m->o[offset++] = 0;

	if (offset < OPT_MIN) {
		memset(&m->o[offset], 0, OPT_MIN - offset);
		offset = OPT_MIN;
	}

	return offset;
}

/**
 * opt_dns_search_dup_ptr() - Look for possible domain name compression pointer
 * @buf:	Current option buffer with existing labels
 * @cmp:	Portion of domain name being added
 * @len:	Length of current option buffer
 *
 * Return: offset to corresponding compression pointer if any, -1 if not found
 */
static int opt_dns_search_dup_ptr(unsigned char *buf, const char *cmp,
				  size_t len)
{
	unsigned int i;

	for (i = 0; i < len; i++) {
		if (buf[i] == 0 &&
		    len - i - 1 >= strlen(cmp) &&
		    !memcmp(buf + i + 1, cmp, strlen(cmp)))
			return i;

		if ((buf[i] & 0xc0) == 0xc0 &&
		    len - i - 2 >= strlen(cmp) &&
		    !memcmp(buf + i + 2, cmp, strlen(cmp)))
			return i + 1;
	}

	return -1;
}

/**
 * opt_set_dns_search() - Fill data and set length for Domain Search option
 * @c:		Execution context
 * @max_len:	Maximum total length of option buffer
 */
static void opt_set_dns_search(const struct ctx *c, size_t max_len)
{
	char buf[NS_MAXDNAME];
	int i;

	opts[119].slen = 0;

	for (i = 0; i < 255; i++)
		max_len -= opts[i].slen;

	for (i = 0; *c->dns_search[i].n; i++) {
		unsigned int n;
		int count = -1;
		const char *p;

		buf[0] = 0;
		for (p = c->dns_search[i].n, n = 1; *p; p++) {
			if (*p == '.') {
				/* RFC 1035 4.1.4 Message compression */
				count = opt_dns_search_dup_ptr(opts[119].s,
							       p + 1,
							       opts[119].slen);

				if (count >= 0) {
					buf[n++] = '\xc0';
					buf[n++] = count;
					break;
				}
				buf[n++] = '.';
			} else {
				buf[n++] = *p;
			}
		}

		/* The compression pointer is also an end of label */
		if (count < 0)
			buf[n++] = 0;

		if (n >= max_len)
			break;

		memcpy(opts[119].s + opts[119].slen, buf, n);
		opts[119].slen += n;
		max_len -= n;
	}

	for (i = 0; i < opts[119].slen; i++) {
		if (!opts[119].s[i] || opts[119].s[i] == '.') {
			opts[119].s[i] = strcspn((char *)opts[119].s + i + 1,
						 ".\xc0");
		}
	}
}

/**
 * dhcp() - Check if this is a DHCP message, reply as needed
 * @c:		Execution context
 * @p:		Packet pool, single packet with Ethernet buffer
 *
 * Return: 0 if it's not a DHCP message, 1 if handled, -1 on failure
 */
int dhcp(const struct ctx *c, const struct pool *p)
{
	size_t mlen, dlen, offset = 0, opt_len, opt_off = 0;
	char macstr[ETH_ADDRSTRLEN];
	const struct ethhdr *eh;
	const struct iphdr *iph;
	const struct udphdr *uh;
	struct in_addr mask;
	unsigned int i;
	struct msg *m;

	eh  = packet_get(p, 0, offset, sizeof(*eh),  NULL);
	offset += sizeof(*eh);

	iph = packet_get(p, 0, offset, sizeof(*iph), NULL);
	if (!eh || !iph)
		return -1;

	offset += iph->ihl * 4UL;
	uh  = packet_get(p, 0, offset, sizeof(*uh),  &mlen);
	offset += sizeof(*uh);

	if (!uh)
		return -1;

	if (uh->dest != htons(67))
		return 0;

	if (c->no_dhcp)
		return 1;

	m   = packet_get(p, 0, offset, offsetof(struct msg, o), &opt_len);
	if (!m						||
	    mlen  != ntohs(uh->len) - sizeof(*uh)	||
	    mlen  <  offsetof(struct msg, o)		||
	    m->op != BOOTREQUEST)
		return -1;

	offset += offsetof(struct msg, o);

	while (opt_off + 2 < opt_len) {
		const uint8_t *olen, *val;
		uint8_t *type;

		type = packet_get(p, 0, offset + opt_off,	1,	NULL);
		olen = packet_get(p, 0, offset + opt_off + 1,	1,	NULL);
		if (!type || !olen)
			return -1;

		val =  packet_get(p, 0, offset + opt_off + 2,	*olen,	NULL);
		if (!val)
			return -1;

		memcpy(&opts[*type].c, val, *olen);
		opts[*type].clen = *olen;
		opt_off += *olen + 2;
	}

	if (opts[53].c[0] == DHCPDISCOVER) {
		info("DHCP: offer to discover");
		opts[53].s[0] = DHCPOFFER;
	} else if (opts[53].c[0] == DHCPREQUEST || !opts[53].clen) {
		info("%s: ack to request", opts[53].clen ? "DHCP" : "BOOTP");
		opts[53].s[0] = DHCPACK;
	} else {
		return -1;
	}

	info("    from %s", eth_ntop(m->chaddr, macstr, sizeof(macstr)));

	m->yiaddr = c->ip4.addr;
	mask.s_addr = htonl(0xffffffff << (32 - c->ip4.prefix_len));
	memcpy(opts[1].s,  &mask,                sizeof(mask));
	memcpy(opts[3].s,  &c->ip4.guest_gw,     sizeof(c->ip4.guest_gw));
	memcpy(opts[54].s, &c->ip4.our_tap_addr, sizeof(c->ip4.our_tap_addr));

	/* If the gateway is not on the assigned subnet, send an option 121
	 * (Classless Static Routing) adding a dummy route to it.
	 */
	if ((c->ip4.addr.s_addr & mask.s_addr)
	    != (c->ip4.guest_gw.s_addr & mask.s_addr)) {
		/* a.b.c.d/32:0.0.0.0, 0:a.b.c.d */
		opts[121].slen = 14;
		opts[121].s[0] = 32;
		memcpy(opts[121].s + 1,
		       &c->ip4.guest_gw, sizeof(c->ip4.guest_gw));
		memcpy(opts[121].s + 10,
		       &c->ip4.guest_gw, sizeof(c->ip4.guest_gw));
	}

	if (c->mtu != -1) {
		opts[26].slen = 2;
		opts[26].s[0] = c->mtu / 256;
		opts[26].s[1] = c->mtu % 256;
	}

	for (i = 0, opts[6].slen = 0;
	     !c->no_dhcp_dns && !IN4_IS_ADDR_UNSPECIFIED(&c->ip4.dns[i]); i++) {
		((struct in_addr *)opts[6].s)[i] = c->ip4.dns[i];
		opts[6].slen += sizeof(uint32_t);
	}

	if (!c->no_dhcp_dns_search)
		opt_set_dns_search(c, sizeof(m->o));

	dlen = offsetof(struct msg, o) + fill(m);
	tap_udp4_send(c, c->ip4.our_tap_addr, 67, c->ip4.addr, 68, m, dlen);

	return 1;
}
