From 4aa8e54a303dfde9a53c47cd07c5af3c5fad4f01 Mon Sep 17 00:00:00 2001 From: Stefano Brivio Date: Tue, 13 Apr 2021 21:59:47 +0200 Subject: [PATCH] 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 --- Makefile | 4 +- README.md | 21 ++- dhcpv6.c | 502 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ dhcpv6.h | 2 + ndp.c | 3 +- passt.c | 17 +- 6 files changed, 530 insertions(+), 19 deletions(-) create mode 100644 dhcpv6.c create mode 100644 dhcpv6.h diff --git a/Makefile b/Makefile index e35ebd7..326e717 100644 --- a/Makefile +++ b/Makefile @@ -2,8 +2,8 @@ CFLAGS += -Wall -Wextra -pedantic all: passt qrap -passt: passt.c passt.h arp.c arp.h dhcp.c dhcp.h ndp.c ndp.h siphash.c siphash.h tap.c tap.h icmp.c icmp.h tcp.c tcp.h udp.c udp.h util.c util.h - $(CC) $(CFLAGS) passt.c arp.c dhcp.c ndp.c siphash.c tap.c icmp.c tcp.c udp.c util.c -o passt +passt: passt.c passt.h arp.c arp.h dhcp.c dhcp.h dhcpv6.c dhcpv6.h ndp.c ndp.h siphash.c siphash.h tap.c tap.h icmp.c icmp.h tcp.c tcp.h udp.c udp.h util.c util.h + $(CC) $(CFLAGS) passt.c arp.c dhcp.c dhcpv6.c ndp.c siphash.c tap.c icmp.c tcp.c udp.c util.c -o passt qrap: qrap.c passt.h $(CC) $(CFLAGS) qrap.c -o qrap diff --git a/README.md b/README.md index dceaf89..1431c81 100644 --- a/README.md +++ b/README.md @@ -171,7 +171,7 @@ capabilities, as we don't need to create any interface. _passt_ provides some minimalistic implementations of networking services that can't practically run on the host: -* [ARP proxy](https://passt.top/passt/tree/arp.c), that resolve the address of +* [ARP proxy](https://passt.top/passt/tree/arp.c), that resolves the address of the host (which is used as gateway) to the original MAC address of the host * [DHCP server](https://passt.top/passt/tree/dhcp.c), a simple implementation handing out one single IPv4 address to the guest, namely, the same address as @@ -179,11 +179,10 @@ can't practically run on the host: nameservers configured on the host * [NDP proxy](https://passt.top/passt/tree/ndp.c), which can also assign prefix and nameserver using SLAAC -* _to be done_: DHCPv6 server: right now, the guest gets the same _prefix_ as - the host, but not the same address, because the suffix is generated from the - MAC address of the virtual machine, so we currently have to translate packet - addresses back and forth. With a DHCPv6 server, we could simply assign the - host address to the guest +* [DHCPv6 server](https://passt.top/passt/tree/dhcpv6.c): a simple + implementation handing out one single IPv6 address to the guest, namely, the + the same address as the first one configured for the upstream host interface, + and passing the first nameserver configured on the host ## Addresses @@ -192,11 +191,11 @@ interface of the host, and the same default gateway as the default gateway of the host. Addresses are never translated. For IPv6, the guest is assigned, via SLAAC, the same prefix as the upstream -interface of the host, and the same default route as the default route of the -host. This means that the guest will typically have a different address, and -the destination address is translated for packets going to the guest. This will -be avoided in the future once a minimalistic DHCPv6 server is implemented in -_passt_. +interface of the host, the same default route as the default route of the +host, and, if a DHCPv6 client is running on the guest, also the same address as +the upstream address of the host. This means that, with a DHCPv6 client on the +guest, addresses don't need to be translated. Should the client use a different +address, the destination address is translated for packets going to the guest. ## Protocols 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; +} diff --git a/dhcpv6.h b/dhcpv6.h new file mode 100644 index 0000000..b29ddcd --- /dev/null +++ b/dhcpv6.h @@ -0,0 +1,2 @@ +int dhcpv6(struct ctx *c, struct ethhdr *eh, size_t len); +void dhcpv6_init(struct ctx *c); diff --git a/ndp.c b/ndp.c index 7468134..1e0d817 100644 --- a/ndp.c +++ b/ndp.c @@ -80,13 +80,14 @@ int ndp(struct ctx *c, struct ethhdr *eh, size_t len) ihr->icmp6_type = RA; ihr->icmp6_code = 0; ihr->icmp6_rt_lifetime = htons(3600); + ihr->icmp6_addrconf_managed = 1; p = (unsigned char *)(ihr + 1); p += 8; /* reachable, retrans time */ *p++ = 3; /* prefix */ *p++ = 4; /* length */ *p++ = 64; /* prefix length */ - *p++ = 0xc0; /* flags: L, A */ + *p++ = 0xc0; /* prefix flags: L, A */ *(uint32_t *)p = htonl(3600); /* lifetime */ p += 4; *(uint32_t *)p = htonl(3600); /* preferred lifetime */ diff --git a/passt.c b/passt.c index 8bc7098..ed8b251 100644 --- a/passt.c +++ b/passt.c @@ -47,6 +47,7 @@ #include "arp.h" #include "dhcp.h" #include "ndp.h" +#include "dhcpv6.h" #include "util.h" #include "icmp.h" #include "tcp.h" @@ -308,6 +309,9 @@ static void tap4_handler(struct ctx *c, char *in, size_t len) char buf_d[BUFSIZ] __attribute((__unused__)); char *l4h; + if (!c->v4) + return; + if (arp(c, eh, len) || dhcp(c, eh, len)) return; @@ -358,17 +362,17 @@ static void tap6_handler(struct ctx *c, char *in, size_t len) uint8_t proto; char *l4h; + if (!c->v6) + return; + if (len < sizeof(*eh) + sizeof(*ip6h)) return; - if (ndp(c, eh, len)) + if (ndp(c, eh, len) || dhcpv6(c, eh, len)) return; l4h = ipv6_l4hdr(ip6h, &proto); - /* TODO: Assign MAC address to guest so that, together with prefix - * assigned via NDP, address matches the one from the host. - */ c->addr6_guest = ip6h->saddr; ip6h->saddr = c->addr6; @@ -559,6 +563,9 @@ int main(int argc, char **argv) if (icmp_sock_init(&c) || tcp_sock_init(&c) || udp_sock_init(&c)) exit(EXIT_FAILURE); + if (c.v6) + dhcpv6_init(&c); + if (c.v4) { info("ARP:"); info(" address: %02x:%02x:%02x:%02x:%02x:%02x from %s", @@ -575,7 +582,7 @@ int main(int argc, char **argv) inet_ntop(AF_INET, &c.dns4, buf4[3], sizeof(buf4[3]))); } if (c.v6) { - info("NDP:"); + info("NDP/DHCPv6:"); info(" assign: %s", inet_ntop(AF_INET6, &c.addr6, buf6[0], sizeof(buf6[0]))); info(" router: %s",