From 4aa8e54a303dfde9a53c47cd07c5af3c5fad4f01 Mon Sep 17 00:00:00 2001 From: Stefano Brivio Date: Tue, 13 Apr 2021 21:59:47 +0200 Subject: passt: Introduce a DHCPv6 server This implementation, similarly to the IPv4 DHCP one, hands out a single address, which is the same as the upstream address for the host. This avoids the need for address translation as long as the client runs a DHCPv6 client. The NDP "Managed" flag is now set in Router Advertisements. Signed-off-by: Stefano Brivio --- dhcpv6.c | 502 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 502 insertions(+) create mode 100644 dhcpv6.c (limited to 'dhcpv6.c') diff --git a/dhcpv6.c b/dhcpv6.c new file mode 100644 index 0000000..687dcff --- /dev/null +++ b/dhcpv6.c @@ -0,0 +1,502 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later + +/* PASST - Plug A Simple Socket Transport + * + * dhcpv6.c - Minimalistic DHCPv6 server for PASST + * + * Copyright (c) 2021 Red Hat GmbH + * Author: Stefano Brivio + * + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "passt.h" +#include "tap.h" +#include "util.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 +#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) +#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 address + */ +struct opt_dns_servers { + struct opt_hdr hdr; + struct in6_addr addr; +}; + +/** + * 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 + * @dns_servers: DNS Recursive Name Server option + * @client_id: Client Identifier, variable length, must be at the end + */ +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_dns_servers dns_servers; + struct opt_client_id client_id; +} __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_DNS_SERVERS, OPT_SIZE(dns_servers), }, + IN6ADDR_ANY_INIT + }, + + { { OPT_CLIENTID, 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; + + if (o->t == type) + return o; + + *len -= ntohs(o->l) + sizeof(struct opt_hdr); + 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; + 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) - sizeof(struct opt_hdr); + + 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))) + 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() - 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_guest = 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))) { + n = OPT_IA_NA ? sizeof(struct opt_ia_na) : + sizeof(struct opt_ia_ta); + 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) + client_id->l); + n += sizeof(struct opt_hdr) + 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)); + resp.uh.len = htons(n = offsetof(struct resp_t, client_id) + + sizeof(struct opt_hdr) + ntohs(client_id->l)); + + resp.hdr.xid = mh->xid; + tap_ip_send(c, &c->gw6, IPPROTO_UDP, (char *)&resp, n); + + return 1; +} + +/** + * dhcpv6() - 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; + resp.dns_servers.addr = c->dns6; +} -- cgit v1.2.3