1
0
Fork 0
mirror of https://passt.top/passt synced 2025-05-21 16:55:34 +02:00
passt/dhcpv6.c
Enrique Llorente 31e8109a86 dhcp, dhcpv6: Add hostname and client fqdn ops
Both DHCPv4 and DHCPv6 has the capability to pass the hostname to
clients, the DHCPv4 uses option 12 (hostname) while the DHCPv6 uses option 39
(client fqdn), for some virt deployments like kubevirt is expected to
have the VirtualMachine name as the guest hostname.

This change add the following arguments:
 - -H --hostname NAME to configure the hostname DHCPv4 option(12)
 - --fqdn NAME to configure client fqdn option for both DHCPv4(81) and
   DHCPv6(39)

Signed-off-by: Enrique Llorente <ellorent@redhat.com>
Signed-off-by: Stefano Brivio <sbrivio@redhat.com>
2025-02-10 18:30:24 +01:00

646 lines
16 KiB
C

// 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
*
* dhcpv6.c - Minimalistic DHCPv6 server for PASST
*
* Copyright (c) 2021 Red Hat GmbH
* Author: Stefano Brivio <sbrivio@redhat.com>
*/
#include <arpa/inet.h>
#include <net/if_arp.h>
#include <net/if.h>
#include <netinet/ip.h>
#include <netinet/udp.h>
#include <netinet/if_ether.h>
#include <stdio.h>
#include <stddef.h>
#include <stdint.h>
#include <unistd.h>
#include <string.h>
#include <time.h>
#include <limits.h>
#include "packet.h"
#include "util.h"
#include "passt.h"
#include "tap.h"
#include "log.h"
/**
* struct opt_hdr - DHCPv6 option header
* @t: Option type
* @l: Option length, network order
*/
struct opt_hdr {
uint16_t t;
# define OPT_CLIENTID htons_constant(1)
# define OPT_SERVERID htons_constant(2)
# define OPT_IA_NA htons_constant(3)
# define OPT_IA_TA htons_constant(4)
# define OPT_IAAADR htons_constant(5)
# define OPT_STATUS_CODE htons_constant(13)
# define STATUS_NOTONLINK htons_constant(4)
# define OPT_DNS_SERVERS htons_constant(23)
# define OPT_DNS_SEARCH htons_constant(24)
# define OPT_CLIENT_FQDN htons_constant(39)
#define STR_NOTONLINK "Prefix not appropriate for link."
uint16_t l;
} __attribute__((packed));
# define OPT_SIZE_CONV(x) (htons_constant(x))
#define OPT_SIZE(x) OPT_SIZE_CONV(sizeof(struct opt_##x) - \
sizeof(struct opt_hdr))
#define OPT_VSIZE(x) (sizeof(struct opt_##x) - \
sizeof(struct opt_hdr))
#define OPT_MAX_SIZE IPV6_MIN_MTU - (sizeof(struct ipv6hdr) + \
sizeof(struct udphdr) + \
sizeof(struct msg_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];
} __attribute__((packed));
/**
* 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];
} __attribute__ ((packed));
#define SERVER_ID { \
{ OPT_SERVERID, OPT_SIZE(server_id) }, \
htons_constant(DUID_TYPE_LLT), \
htons_constant(ARPHRD_ETHER), 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;
} __attribute__((packed));
/**
* 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;
} __attribute__((packed));
/**
* 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;
} __attribute__((packed));
/**
* 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];
} __attribute__((packed));
/**
* 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];
} __attribute__((packed));
/**
* 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];
} __attribute__((packed));
/**
* struct opt_client_fqdn - Client FQDN option (RFC 4704)
* @hdr: Option header
* @flags: Flags described by RFC 4704
* @domain_name: Client FQDN
*/
struct opt_client_fqdn {
struct opt_hdr hdr;
uint8_t flags;
char domain_name[PASST_MAXDNAME];
} __attribute__((packed));
/**
* 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__));
/**
* struct resp_t - Normal advertise and reply message
* @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
* @client_fqdn: Client FQDN, variable length
*/
static struct resp_t {
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;
struct opt_client_fqdn client_fqdn;
} __attribute__((__packed__)) resp = {
{ 0 },
SERVER_ID,
{ { 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 },
},
{ { OPT_CLIENT_FQDN, 0, },
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.)
* @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 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 = {
{ TYPE_REPLY, 0 },
SERVER_ID,
{ 0, },
};
/**
* dhcpv6_opt() - Get option from DHCPv6 message
* @p: Packet pool, single packet with UDP header
* @offset: Offset to look at, 0: end of header, set to option start
* @type: Option type to look up, network order
*
* Return: pointer to option header, or NULL on malformed or missing option
*/
static struct opt_hdr *dhcpv6_opt(const struct pool *p, size_t *offset,
uint16_t type)
{
struct opt_hdr *o;
size_t left;
if (!*offset)
*offset = sizeof(struct udphdr) + sizeof(struct msg_hdr);
while ((o = packet_get_try(p, 0, *offset, sizeof(*o), &left))) {
unsigned int opt_len = ntohs(o->l) + sizeof(*o);
if (ntohs(o->l) > left)
return NULL;
if (o->t == type)
return o;
*offset += opt_len;
}
return NULL;
}
/**
* dhcpv6_ia_notonlink() - Check if any IA contains non-appropriate addresses
* @p: Packet pool, single packet starting from UDP header
* @la: 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(const struct pool *p,
struct in6_addr *la)
{
int ia_types[2] = { OPT_IA_NA, OPT_IA_TA }, *ia_type;
const struct opt_ia_addr *opt_addr;
char buf[INET6_ADDRSTRLEN];
struct in6_addr req_addr;
const struct opt_hdr *h;
struct opt_hdr *ia;
size_t offset;
foreach(ia_type, ia_types) {
offset = 0;
while ((ia = dhcpv6_opt(p, &offset, *ia_type))) {
if (ntohs(ia->l) < OPT_VSIZE(ia_na))
return NULL;
offset += sizeof(struct opt_ia_na);
while ((h = dhcpv6_opt(p, &offset, OPT_IAAADR))) {
if (ntohs(h->l) != OPT_VSIZE(ia_addr))
return NULL;
opt_addr = (const struct opt_ia_addr *)h;
req_addr = opt_addr->addr;
if (!IN6_ARE_ADDR_EQUAL(la, &req_addr))
goto err;
offset += sizeof(struct opt_ia_addr);
}
}
}
return NULL;
err:
info("DHCPv6: requested address %s not on link",
inet_ntop(AF_INET6, &req_addr, buf, sizeof(buf)));
return ia;
}
/**
* 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(const struct ctx *c, char *buf, int offset)
{
struct opt_dns_servers *srv = NULL;
struct opt_dns_search *srch = NULL;
int i;
if (c->no_dhcp_dns)
goto search;
for (i = 0; !IN6_IS_ADDR_UNSPECIFIED(&c->ip6.dns[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;
}
srv->addr[i] = c->ip6.dns[i];
srv->hdr.l += sizeof(srv->addr[i]);
offset += sizeof(srv->addr[i]);
}
if (srv)
srv->hdr.l = htons(srv->hdr.l);
search:
if (c->no_dhcp_dns_search)
return offset;
for (i = 0; *c->dns_search[i].n; i++) {
size_t name_len = strlen(c->dns_search[i].n);
/* We already append separators, don't duplicate if present */
if (c->dns_search[i].n[name_len - 1] == '.')
name_len--;
/* Skip root-only search domains */
if (!name_len)
continue;
name_len += 2; /* Length byte for first label, and terminator */
if (name_len >
NS_MAXDNAME + 1 /* Length byte for first label */ ||
name_len > 255) {
debug("DHCP: DNS search name '%s' too long, skipping",
c->dns_search[i].n);
continue;
}
if (!srch) {
srch = (struct opt_dns_search *)(buf + offset);
offset += sizeof(struct opt_hdr);
srch->hdr.t = OPT_DNS_SEARCH;
srch->hdr.l = 0;
}
encode_domain_name(buf + offset, c->dns_search[i].n);
srch->hdr.l += name_len;
offset += name_len;
}
if (srch)
srch->hdr.l = htons(srch->hdr.l);
return offset;
}
/**
* dhcpv6_client_fqdn_fill() - Fill in client FQDN option
* @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_client_fqdn_fill(const struct pool *p, const struct ctx *c,
char *buf, int offset)
{
struct opt_client_fqdn const *req_opt;
struct opt_client_fqdn *o;
size_t opt_len;
opt_len = strlen(c->fqdn);
if (opt_len == 0) {
return offset;
}
opt_len += 2; /* Length byte for first label, and terminator */
if (opt_len > OPT_MAX_SIZE - (offset +
sizeof(struct opt_hdr) +
1 /* flags */ )) {
debug("DHCPv6: client FQDN option doesn't fit, skipping");
return offset;
}
o = (struct opt_client_fqdn *)(buf + offset);
encode_domain_name(o->domain_name, c->fqdn);
req_opt = (struct opt_client_fqdn *)dhcpv6_opt(p, &(size_t){ 0 },
OPT_CLIENT_FQDN);
if (req_opt && req_opt->flags & 0x01 /* S flag */)
o->flags = 0x02 /* O flag */;
else
o->flags = 0x00;
opt_len++;
o->hdr.t = OPT_CLIENT_FQDN;
o->hdr.l = htons(opt_len);
return offset + sizeof(struct opt_hdr) + opt_len;
}
/**
* dhcpv6() - Check if this is a DHCPv6 message, reply as needed
* @c: Execution context
* @p: Packet pool, single packet starting from UDP header
* @saddr: Source IPv6 address of original message
* @daddr: Destination IPv6 address of original message
*
* Return: 0 if it's not a DHCPv6 message, 1 if handled, -1 on failure
*/
int dhcpv6(struct ctx *c, const struct pool *p,
const struct in6_addr *saddr, const struct in6_addr *daddr)
{
const struct opt_hdr *client_id, *server_id, *ia;
const struct in6_addr *src;
const struct msg_hdr *mh;
const struct udphdr *uh;
struct opt_hdr *bad_ia;
size_t mlen, n;
uh = packet_get(p, 0, 0, sizeof(*uh), &mlen);
if (!uh)
return -1;
if (uh->dest != htons(547))
return 0;
if (c->no_dhcpv6)
return 1;
if (!IN6_IS_ADDR_MULTICAST(daddr))
return -1;
if (mlen + sizeof(*uh) != ntohs(uh->len) || mlen < sizeof(*mh))
return -1;
c->ip6.addr_ll_seen = *saddr;
src = &c->ip6.our_tap_ll;
mh = packet_get(p, 0, sizeof(*uh), sizeof(*mh), NULL);
if (!mh)
return -1;
client_id = dhcpv6_opt(p, &(size_t){ 0 }, OPT_CLIENTID);
if (!client_id || ntohs(client_id->l) > OPT_VSIZE(client_id))
return -1;
server_id = dhcpv6_opt(p, &(size_t){ 0 }, OPT_SERVERID);
if (server_id && ntohs(server_id->l) != OPT_VSIZE(server_id))
return -1;
ia = dhcpv6_opt(p, &(size_t){ 0 }, OPT_IA_NA);
if (ia && ntohs(ia->l) < MIN(OPT_VSIZE(ia_na), OPT_VSIZE(ia_ta)))
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(p, &c->ip6.addr))) {
info("DHCPv6: received CONFIRM with inappropriate IA,"
" sending NotOnLink status in REPLY");
bad_ia->l = htons(OPT_VSIZE(ia_na) +
sizeof(sc_not_on_link));
n = sizeof(struct opt_ia_na);
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.hdr.xid = mh->xid;
tap_udp6_send(c, src, 547, tap_ip6_daddr(c, src), 546,
mh->xid, &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;
if (ia || dhcpv6_opt(p, &(size_t){ 0 }, OPT_IA_TA))
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);
n = dhcpv6_client_fqdn_fill(p, c, (char *)&resp, n);
resp.hdr.xid = mh->xid;
tap_udp6_send(c, src, 547, tap_ip6_daddr(c, src), 546,
mh->xid, &resp, n);
c->ip6.addr_seen = c->ip6.addr;
return 1;
}
/**
* dhcpv6_init() - Initialise DUID and addresses for DHCPv6 server
* @c: Execution context
*/
void dhcpv6_init(const struct ctx *c)
{
time_t y2k = 946684800; /* Epoch to 2000-01-01T00:00:00Z, no mktime() */
uint32_t duid_time;
duid_time = htonl(difftime(time(NULL), 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->our_tap_mac, sizeof(c->our_tap_mac));
memcpy(resp_not_on_link.server_id.duid_lladdr,
c->our_tap_mac, sizeof(c->our_tap_mac));
resp.ia_addr.addr = c->ip6.addr;
}