aboutgitcodebugslistschat
path: root/dhcpv6.c
blob: be6d9b143011742e3d8dfe689186f3e905bc82cb (plain) (tree)
1
2
3
4
5
6
7


                                             



                                         




                                                  







                   
                      





                           
 
                 

                  
















                                            
                                  








                                                       
                                                       








































































































                                                                                
                                          


                           










                                                                





































                                                                 


                                                                             







                                       
                                       

                                           














                                                                          


                                     







                                     










































                                                                                

                                                             


                                 

















                                                                             
                                   









                                                        
                                             

















                                                                              




                                                                                
                                          
                         



















                                                                 




























































                                                                           





























                                                                      
                                       






























                                                                               






                                                                              
 
                                                                         


                                                       


                                                                             















































                                                                                




                                                         

                               
 
                                                               
                                 




                 
                                                                  















                                                                                
 
// SPDX-License-Identifier: AGPL-3.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
 *
 * dhcpv6.c - Minimalistic DHCPv6 server for PASST
 *
 * Copyright (c) 2021 Red Hat GmbH
 * Author: Stefano Brivio <sbrivio@redhat.com>
 */

#include <stdio.h>
#include <stddef.h>
#include <stdint.h>
#include <unistd.h>
#include <string.h>
#include <time.h>
#include <arpa/inet.h>
#include <linux/if_ether.h>
#include <linux/ip.h>
#include <linux/ipv6.h>
#include <linux/udp.h>
#include <net/if.h>
#include <net/if_arp.h>

#include "util.h"
#include "passt.h"
#include "tap.h"

/**
 * struct opt_hdr - DHCPv6 option header
 * @t:		Option type
 * @l:		Option length, network order
 */
struct opt_hdr {
	uint16_t t;
#if __BYTE_ORDER == __BIG_ENDIAN
# define OPT_CLIENTID		1
# define OPT_SERVERID		2
# define OPT_IA_NA		3
# define OPT_IA_TA		4
# define OPT_IAAADR		5
# define OPT_STATUS_CODE	13
# define  STATUS_NOTONLINK	4
# define OPT_DNS_SERVERS	23
# define OPT_DNS_SEARCH		24
#else
# define OPT_CLIENTID		__bswap_constant_16(1)
# define OPT_SERVERID		__bswap_constant_16(2)
# define OPT_IA_NA		__bswap_constant_16(3)
# define OPT_IA_TA		__bswap_constant_16(4)
# define OPT_IAAADR		__bswap_constant_16(5)
# define OPT_STATUS_CODE	__bswap_constant_16(13)
# define  STATUS_NOTONLINK	__bswap_constant_16(4)
# define OPT_DNS_SERVERS	__bswap_constant_16(23)
# define OPT_DNS_SEARCH		__bswap_constant_16(24)
#endif
#define   STR_NOTONLINK		"Prefix not appropriate for link."

	uint16_t l;
} __attribute__((__packed__));

#if __BYTE_ORDER == __BIG_ENDIAN
# define OPT_SIZE_CONV(x)	(x)
#else
# define OPT_SIZE_CONV(x)	(__bswap_constant_16(x))
#endif
#define OPT_SIZE(x)		OPT_SIZE_CONV(sizeof(struct opt_##x) -	\
					      sizeof(struct opt_hdr))

/**
 * struct opt_client_id - DHCPv6 Client Identifier option
 * @hdr:		Option header
 * @duid:		Client DUID, up to 128 bytes (cf. RFC 8415, 11.1.)
 */
struct opt_client_id {
	struct opt_hdr hdr;
	uint8_t duid[128];
};

/**
 * struct opt_server_id - DHCPv6 Server Identifier option
 * @hdr:		Option header
 * @duid_type:		Type of server DUID, network order
 * @duid_hw:		IANA hardware type, network order
 * @duid_time:		Time reference, network order
 * @duid_lladdr:	Link-layer address (MAC address)
 */
struct opt_server_id {
	struct opt_hdr hdr;
	uint16_t duid_type;
#define DUID_TYPE_LLT		1

	uint16_t duid_hw;
	uint32_t duid_time;
	uint8_t duid_lladdr[ETH_ALEN];
};

static const struct opt_server_id server_id_const = {
	{ OPT_SERVERID,		OPT_SIZE(server_id) },
#if __BYTE_ORDER == __BIG_ENDIAN
	  DUID_TYPE_LLT, ARPHRD_ETHER,
#else
	  __bswap_constant_16(DUID_TYPE_LLT), __bswap_constant_16(ARPHRD_ETHER),
#endif
	  0, { 0 }
};

/**
 * struct opt_ia_na - Identity Association for Non-temporary Addresses Option
 * @hdr:		Option header
 * @iaid:		Unique identifier for IA_NA, network order
 * @t1:			Rebind interval for this server (always infinity)
 * @t2:			Rebind interval for any server (always infinity)
 */
struct opt_ia_na {
	struct opt_hdr hdr;
	uint32_t iaid;
	uint32_t t1;
	uint32_t t2;
};

/**
 * struct opt_ia_ta - Identity Association for Temporary Addresses Option
 * @hdr:		Option header
 * @iaid:		Unique identifier for IA_TA, network order
 */
struct opt_ia_ta {
	struct opt_hdr hdr;
	uint32_t iaid;
};

/**
 * struct opt_ia_addr - IA Address Option
 * @hdr:		Option header
 * @addr:		Leased IPv6 address
 * @pref_lifetime:	Preferred lifetime, network order (always infinity)
 * @valid_lifetime:	Valid lifetime, network order (always infinity)
 */
struct opt_ia_addr {
	struct opt_hdr hdr;
	struct in6_addr addr;
	uint32_t pref_lifetime;
	uint32_t valid_lifetime;
};

/**
 * struct opt_status_code - Status Code Option (used for NotOnLink error only)
 * @hdr:		Option header
 * @code:		Numeric code for status, network order
 * @status_msg:		Text string suitable for display, not NULL-terminated
 */
struct opt_status_code {
	struct opt_hdr hdr;
	uint16_t code;
	char status_msg[sizeof(STR_NOTONLINK) - 1];
};

/**
 * struct opt_dns_servers - DNS Recursive Name Server option (RFC 3646)
 * @hdr:		Option header
 * @addr:		IPv6 DNS addresses
 */
struct opt_dns_servers {
	struct opt_hdr hdr;
	struct in6_addr addr[MAXNS];
};

/**
 * struct opt_dns_servers - Domain Search List option (RFC 3646)
 * @hdr:		Option header
 * @list:		NULL-separated list of domain names
 */
struct opt_dns_search {
	struct opt_hdr hdr;
	char list[MAXDNSRCH * NS_MAXDNAME];
};

/**
 * struct msg_hdr - DHCPv6 client/server message header
 * @type:		DHCP message type
 * @xid:		Transaction ID for message exchange
 */
struct msg_hdr {
	uint32_t type:8;
#define TYPE_SOLICIT			1
#define TYPE_ADVERTISE			2
#define TYPE_REQUEST			3
#define TYPE_CONFIRM			4
#define TYPE_RENEW			5
#define TYPE_REBIND			6
#define TYPE_REPLY			7
#define TYPE_RELEASE			8
#define TYPE_DECLINE			9
#define TYPE_INFORMATION_REQUEST	11

	uint32_t xid:24;
} __attribute__((__packed__));

static const struct udphdr uh_resp = {
#if __BYTE_ORDER == __BIG_ENDIAN
	547, 546, 0, 0,
#else
	__bswap_constant_16(547), __bswap_constant_16(546), 0, 0,
#endif
};

/**
 * struct resp_t - Normal advertise and reply message
 * @uh:			UDP header
 * @hdr:		DHCP message header
 * @server_id:		Server Identifier option
 * @ia_na:		Non-temporary Address option
 * @ia_addr:		Address for IA_NA
 * @client_id:		Client Identifier, variable length
 * @dns_servers:	DNS Recursive Name Server, here just for storage size
 * @dns_search:		Domain Search List, here just for storage size
 */
static struct resp_t {
	struct udphdr  uh;
	struct msg_hdr hdr;

	struct opt_server_id server_id;
	struct opt_ia_na ia_na;
	struct opt_ia_addr ia_addr;
	struct opt_client_id client_id;
	struct opt_dns_servers dns_servers;
	struct opt_dns_search dns_search;
} __attribute__((__packed__)) resp = {
	uh_resp,
	{ 0 },
	server_id_const,

	{ { OPT_IA_NA,		OPT_SIZE_CONV(sizeof(struct opt_ia_na) +
					      sizeof(struct opt_ia_addr) -
					      sizeof(struct opt_hdr)) },
	  1, (uint32_t)~0U, (uint32_t)~0U
	},

	{ { OPT_IAAADR,		OPT_SIZE(ia_addr) },
	  IN6ADDR_ANY_INIT, (uint32_t)~0U, (uint32_t)~0U
	},

	{ { OPT_CLIENTID,	0, },
	  { 0 }
	},

	{ { OPT_DNS_SERVERS,	0, },
	  { IN6ADDR_ANY_INIT }
	},

	{ { OPT_DNS_SEARCH,	0, },
	  { 0 },
	},
};

static const struct opt_status_code sc_not_on_link = {
	{ OPT_STATUS_CODE,	OPT_SIZE(status_code), },
	STATUS_NOTONLINK, STR_NOTONLINK
};

/**
 * struct resp_not_on_link_t - NotOnLink error (mandated by RFC 8415, 18.3.2.)
 * @uh:			UDP header
 * @hdr:		DHCP message header
 * @server_id:		Server Identifier option
 * @var:		Payload: IA_NA from client, status code, client ID
 */
static struct resp_not_on_link_t {
	struct udphdr  uh;
	struct msg_hdr hdr;

	struct opt_server_id server_id;

	uint8_t var[sizeof(struct opt_ia_na) + sizeof(struct opt_status_code) +
		    sizeof(struct opt_client_id)];
} __attribute__((__packed__)) resp_not_on_link = {
	uh_resp,
	{ TYPE_REPLY, 0 },
	server_id_const,
	{ 0, },
};

/**
 * dhcpv6_opt() - Get option from DHCPv6 message
 * @o:			First option header to check
 * @type:		Option type to look up, network order
 * @len:		Remaining length, host order, modified on return
 *
 * Return: pointer to option header, or NULL on malformed or missing option
 */
static struct opt_hdr *dhcpv6_opt(struct opt_hdr *o, uint16_t type, size_t *len)
{
	while (*len >= sizeof(struct opt_hdr)) {
		if (ntohs(o->l) > *len)
			return NULL;

		*len -= ntohs(o->l) + sizeof(struct opt_hdr);

		if (o->t == type)
			return o;

		o = (struct opt_hdr *)((uint8_t *)(o + 1) + ntohs(o->l));
	}

	return NULL;
}

/**
 * dhcpv6_ia_notonlink() - Check if any IA contains non-appropriate addresses
 * @o:			First option header to check for IAs
 * @len:		Remaining message length, host order
 * @addr:		Address we want to lease to the client
 *
 * Return: pointer to non-appropriate IA_NA or IA_TA, if any, NULL otherwise
 */
static struct opt_hdr *dhcpv6_ia_notonlink(struct opt_hdr *o, size_t len,
					   struct in6_addr *addr)
{
	struct opt_hdr *ia, *ia_addr;
	char buf[INET6_ADDRSTRLEN];
	struct in6_addr *req_addr;
	size_t __len;
	int ia_type;

	ia_type = OPT_IA_NA;
ia_ta:
	__len = len;
	ia = o;

	while ((ia = dhcpv6_opt(ia, ia_type, &__len))) {
		size_t ia_len = ntohs(ia->l);

		if (ia_len > __len)
			return NULL;

		if (ia_type == OPT_IA_NA) {
			struct opt_ia_na *opts = (struct opt_ia_na *)ia + 1;

			ia_addr = (struct opt_hdr *)opts;
		} else if (ia_type == OPT_IA_TA) {
			struct opt_ia_ta *opts = (struct opt_ia_ta *)ia + 1;

			ia_addr = (struct opt_hdr *)opts;
		}

		while ((ia_addr = dhcpv6_opt(ia_addr, OPT_IAAADR, &ia_len))) {
			struct opt_ia_addr *next;

			req_addr = (struct in6_addr *)(ia_addr + 1);

			if (memcmp(addr, req_addr, sizeof(*addr))) {
				info("DHCPv6: requested address %s not on link",
				     inet_ntop(AF_INET6, req_addr,
					       buf, sizeof(buf)));
				return ia;
			}

			next = (struct opt_ia_addr *)ia_addr + 1;
			ia_addr = (struct opt_hdr *)next;
		}

		if (!ia_addr)
			break;

		ia = ia_addr;
	}

	if (ia_type == OPT_IA_NA) {
		ia_type = OPT_IA_TA;
		goto ia_ta;
	}

	return NULL;
}

/**
 * dhcpv6_dns_fill() - Fill in DNS Servers and Domain Search list options
 * @c:		Execution context
 * @buf:	Response message buffer where options will be appended
 * @offset:	Offset in message buffer for new options
 *
 * Return: updated length of response message buffer.
 */
static size_t dhcpv6_dns_fill(struct ctx *c, char *buf, int offset)
{
	struct opt_dns_servers *srv = NULL;
	struct opt_dns_search *srch = NULL;
	int i;

	for (i = 0; !IN6_IS_ADDR_UNSPECIFIED(&c->dns6[i]); i++) {
		if (!i) {
			srv = (struct opt_dns_servers *)(buf + offset);
			offset += sizeof(struct opt_hdr);
			srv->hdr.t = OPT_DNS_SERVERS;
			srv->hdr.l = 0;
		}

		memcpy(&srv->addr[i], &c->dns6[i], sizeof(srv->addr[i]));
		srv->hdr.l += sizeof(srv->addr[i]);
		offset += sizeof(srv->addr[i]);
	}

	if (srv)
		srv->hdr.l = htons(srv->hdr.l);

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

		if (!i) {
			srch = (struct opt_dns_search *)(buf + offset);
			offset += sizeof(struct opt_hdr);
			srch->hdr.t = OPT_DNS_SEARCH;
			srch->hdr.l = 0;
			p = srch->list;
			*p = 0;
		}

		p = stpcpy(p + 1, c->dns_search[i].n);
		*(p++) = 0;
		srch->hdr.l += strlen(c->dns_search[i].n) + 2;
		offset += strlen(c->dns_search[i].n) + 2;
	}

	if (srch) {
		for (i = 0; i < srch->hdr.l; i++) {
			if (srch->list[i] == '.' || !srch->list[i]) {
				srch->list[i] = strcspn(srch->list + i + 1,
							".");
			}
		}
		srch->hdr.l = htons(srch->hdr.l);
	}

	return offset;
}

/**
 * dhcpv6() - Check if this is a DHCPv6 message, reply as needed
 * @c:		Execution context
 * @eh:		Packet buffer, Ethernet header
 * @len:	Total L2 packet length
 *
 * Return: 0 if it's not a DHCPv6 message, 1 if handled, -1 on failure
 */
int dhcpv6(struct ctx *c, struct ethhdr *eh, size_t len)
{
	struct ipv6hdr *ip6h = (struct ipv6hdr *)(eh + 1);
	struct opt_hdr *ia, *bad_ia, *client_id, *server_id;
	struct msg_hdr *mh;
	struct udphdr *uh;
	uint8_t proto;
	size_t mlen;
	size_t n;

	uh = (struct udphdr *)ipv6_l4hdr(ip6h, &proto);
	if (!uh || proto != IPPROTO_UDP || uh->dest != htons(547))
		return 0;

	if (!IN6_IS_ADDR_MULTICAST(&ip6h->daddr))
		return -1;

	mlen = len - ((intptr_t)uh - (intptr_t)eh) - sizeof(*uh);

	if (mlen != ntohs(uh->len) - sizeof(*uh) ||
	    mlen < sizeof(struct msg_hdr))
		return -1;

	c->addr6_ll_seen = ip6h->saddr;

	mh = (struct msg_hdr *)(uh + 1);
	mlen -= sizeof(struct msg_hdr);

	n = mlen;
	client_id = dhcpv6_opt((struct opt_hdr *)(mh + 1), OPT_CLIENTID, &n);
	if (!client_id || ntohs(client_id->l) > ntohs(OPT_SIZE(client_id)))
		return -1;

	n = mlen;
	server_id = dhcpv6_opt((struct opt_hdr *)(mh + 1), OPT_SERVERID, &n);

	n = mlen;
	ia = dhcpv6_opt((struct opt_hdr *)(mh + 1), OPT_IA_NA, &n);
	if (ia && ntohs(ia->l) < ntohs(OPT_SIZE(ia_na)))
		return -1;

	resp.hdr.type = TYPE_REPLY;
	switch (mh->type) {
	case TYPE_REQUEST:
	case TYPE_RENEW:
		if (!server_id ||
		    memcmp(&resp.server_id, server_id, sizeof(resp.server_id)))
			return -1;
		/* Falls through */
	case TYPE_CONFIRM:
		if (mh->type == TYPE_CONFIRM && server_id)
			return -1;

		if ((bad_ia = dhcpv6_ia_notonlink((struct opt_hdr *)(mh + 1),
						  mlen, &c->addr6))) {
			info("DHCPv6: received CONFIRM with inappropriate IA,"
			     " sending NotOnLink status in REPLY");

			n = ntohs(bad_ia->l) + sizeof(struct opt_hdr);
			bad_ia->l = htons(n - sizeof(struct opt_hdr) +
					  sizeof(sc_not_on_link));
			memcpy(resp_not_on_link.var, bad_ia, n);

			memcpy(resp_not_on_link.var + n, &sc_not_on_link,
			       sizeof(sc_not_on_link));
			n += sizeof(sc_not_on_link);

			memcpy(resp_not_on_link.var + n, client_id,
			       sizeof(struct opt_hdr) + ntohs(client_id->l));
			n += sizeof(struct opt_hdr) + ntohs(client_id->l);

			n = offsetof(struct resp_not_on_link_t, var) + n;
			resp_not_on_link.uh.len = htons(n);

			resp_not_on_link.hdr.xid = mh->xid;

			tap_ip_send(c, &c->gw6, IPPROTO_UDP,
				    (char *)&resp_not_on_link, n);

			return 1;
		}

		info("DHCPv6: received REQUEST/RENEW/CONFIRM, sending REPLY");
		break;
	case TYPE_INFORMATION_REQUEST:
		if (server_id &&
		    memcmp(&resp.server_id, server_id, sizeof(resp.server_id)))
			return -1;

		n = mlen;
		if (ia || dhcpv6_opt((struct opt_hdr *)(mh + 1), OPT_IA_TA, &n))
			return -1;

		info("DHCPv6: received INFORMATION_REQUEST, sending REPLY");
		break;
	case TYPE_REBIND:
		if (!server_id ||
		    memcmp(&resp.server_id, server_id, sizeof(resp.server_id)))
			return -1;

		info("DHCPv6: received REBIND, sending REPLY");
		break;
	case TYPE_SOLICIT:
		if (server_id)
			return -1;

		resp.hdr.type = TYPE_ADVERTISE;

		info("DHCPv6: received SOLICIT, sending ADVERTISE");
		break;
	default:
		return -1;
	}
	if (ia)
		resp.ia_na.iaid = ((struct opt_ia_na *)ia)->iaid;

	memcpy(&resp.client_id, client_id,
	       ntohs(client_id->l) + sizeof(struct opt_hdr));

	n = offsetof(struct resp_t, client_id) +
	    sizeof(struct opt_hdr) + ntohs(client_id->l);
	n = dhcpv6_dns_fill(c, (char *)&resp, n);
	resp.uh.len = htons(n);

	resp.hdr.xid = mh->xid;

	tap_ip_send(c, &c->gw6, IPPROTO_UDP, (char *)&resp, n);
	c->addr6_seen = c->addr6;

	return 1;
}

/**
 * dhcpv6_init() - Initialise DUID and addresses for DHCPv6 server
 * @c:		Execution context
 */
void dhcpv6_init(struct ctx *c)
{
	struct tm y2k = { 0, 0, 0, 1, 0, 100, 0, 0, 0, 0, NULL };
	uint32_t duid_time;

	duid_time = htonl(difftime(time(NULL), mktime(&y2k)));

	resp.server_id.duid_time		= duid_time;
	resp_not_on_link.server_id.duid_time	= duid_time;

	memcpy(resp.server_id.duid_lladdr,		c->mac, sizeof(c->mac));
	memcpy(resp_not_on_link.server_id.duid_lladdr,	c->mac, sizeof(c->mac));

	resp.ia_addr.addr	= c->addr6;
}