From 9010054ea4ceee9105aa938f15b79a3a91ec5969 Mon Sep 17 00:00:00 2001 From: Stefano Brivio Date: Fri, 21 May 2021 11:14:47 +0200 Subject: [PATCH] dhcp, ndp, dhcpv6: Support for multiple DNS servers, search list Add support for a variable amount of DNS servers, including zero, from /etc/resolv.conf, in DHCP, NDP and DHCPv6 implementations. Introduce support for domain search list for DHCP (RFC 3397), NDP (RFC 8106), and DHCPv6 (RFC 3646), also sourced from /etc/resolv.conf. Signed-off-by: Stefano Brivio --- dhcp.c | 97 ++++++++++++++++++++++++++++++++++++++++++++++++-- dhcpv6.c | 105 ++++++++++++++++++++++++++++++++++++++++++++++++++----- ndp.c | 54 +++++++++++++++++++++++----- passt.c | 77 +++++++++++++++++++++++++++++----------- passt.h | 17 ++++++--- util.h | 7 +++- 6 files changed, 312 insertions(+), 45 deletions(-) diff --git a/dhcp.c b/dhcp.c index cff9403..6448e51 100644 --- a/dhcp.c +++ b/dhcp.c @@ -45,7 +45,6 @@ struct opt { static struct opt opts[255] = { [1] = { 0, 4, { 0 }, 0, { 0 }, }, /* Mask */ [3] = { 0, 4, { 0 }, 0, { 0 }, }, /* Router */ - [6] = { 0, 4, { 0 }, 0, { 0 }, }, /* DNS */ [51] = { 0, 4, { 0xff, 0xff, 0xff, 0xff }, 0, { 0 }, }, /* Lease time */ [53] = { 0, 1, { 0 }, 0, { 0 }, }, /* Type */ #define DHCPDISCOVER 1 @@ -154,6 +153,92 @@ static int fill(struct msg *m) 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, 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(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 dup = -1; + 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 */ + dup = opt_dns_search_dup_ptr(opts[119].s, p + 1, + opts[119].slen); + + if (dup >= 0) { + buf[n++] = '\xc0'; + buf[n++] = dup; + break; + } else { + buf[n++] = '.'; + } + } else { + buf[n++] = *p; + } + } + + /* The compression pointer is also an end of label */ + if (dup < 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 @@ -213,10 +298,16 @@ int dhcp(struct ctx *c, struct ethhdr *eh, size_t len) m->yiaddr = c->addr4; *(unsigned long *)opts[1].s = c->mask4; *(unsigned long *)opts[3].s = c->gw4; - *(unsigned long *)opts[6].s = c->dns4; *(unsigned long *)opts[54].s = c->gw4; - uh->len = htons(len = offsetof(struct msg, o) + fill(m)); + for (i = 0, opts[6].slen = 0; c->dns4[i]; i++) { + ((uint32_t *)opts[6].s)[i] = c->dns4[i]; + opts[6].slen += sizeof(uint32_t); + } + + opt_set_dns_search(c, sizeof(m->o)); + + uh->len = htons(len = offsetof(struct msg, o) + fill(m) + sizeof(*uh)); uh->check = 0; uh->source = htons(67); uh->dest = htons(68); diff --git a/dhcpv6.c b/dhcpv6.c index 6e006dd..4ce7a87 100644 --- a/dhcpv6.c +++ b/dhcpv6.c @@ -15,13 +15,13 @@ #include #include #include +#include #include #include #include #include #include #include -#include #include "passt.h" #include "tap.h" @@ -43,6 +43,7 @@ struct opt_hdr { # 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) @@ -52,6 +53,7 @@ struct opt_hdr { # 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." @@ -157,11 +159,21 @@ struct opt_status_code { /** * struct opt_dns_servers - DNS Recursive Name Server option (RFC 3646) * @hdr: Option header - * @addr: IPv6 DNS address + * @addr: IPv6 DNS addresses */ struct opt_dns_servers { struct opt_hdr hdr; - struct in6_addr addr; + 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]; }; /** @@ -200,8 +212,9 @@ static const struct udphdr uh_resp = { * @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 + * @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; @@ -211,6 +224,8 @@ static struct resp_t { 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 }, @@ -226,10 +241,17 @@ static struct resp_t { 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 = { @@ -352,6 +374,67 @@ 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 @@ -478,10 +561,14 @@ int dhcpv6(struct ctx *c, struct ethhdr *eh, size_t len) 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)); + + 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; @@ -489,7 +576,7 @@ int dhcpv6(struct ctx *c, struct ethhdr *eh, size_t len) } /** - * dhcpv6() - Initialise DUID and addresses for DHCPv6 server + * dhcpv6_init() - Initialise DUID and addresses for DHCPv6 server * @c: Execution context */ void dhcpv6_init(struct ctx *c) diff --git a/ndp.c b/ndp.c index 1e0d817..40cfe93 100644 --- a/ndp.c +++ b/ndp.c @@ -14,6 +14,7 @@ #include #include #include +#include #include #include #include @@ -21,7 +22,6 @@ #include #include #include -#include #include "passt.h" #include "util.h" @@ -76,6 +76,9 @@ int ndp(struct ctx *c, struct ethhdr *eh, size_t len) memcpy(p, c->mac, ETH_ALEN); p += 6; } else if (ih->icmp6_type == RS) { + size_t len = 0; + int i, n; + info("NDP: received RS, sending RA"); ihr->icmp6_type = RA; ihr->icmp6_code = 0; @@ -95,13 +98,48 @@ int ndp(struct ctx *c, struct ethhdr *eh, size_t len) memcpy(p, &c->addr6, 8); /* prefix */ p += 16; - *p++ = 25; /* RDNS */ - *p++ = 3; /* length */ - p += 2; - *(uint32_t *)p = htonl(60); /* lifetime */ - p += 4; - memcpy(p, &c->dns6, 16); /* address */ - p += 16; + for (n = 0; !IN6_IS_ADDR_UNSPECIFIED(&c->dns6[n]); n++); + if (n) { + *p++ = 25; /* RDNSS */ + *p++ = 1 + 2 * n; /* length */ + p += 2; /* reserved */ + *(uint32_t *)p = htonl(60); /* lifetime */ + p += 4; + + for (i = 0; i < n; i++) { + memcpy(p, &c->dns6[i], 16); /* address */ + p += 16; + } + } + + for (n = 0; *c->dns_search[n].n; n++) + len += strlen(c->dns_search[n].n) + 2; + if (len) { + *p++ = 31; /* DNSSL */ + *p++ = 2 + (len + 8 - 1) / 8; /* length */ + p += 2; /* reserved */ + *(uint32_t *)p = htonl(60); /* lifetime */ + p += 4; + + for (i = 0; i < n; i++) { + char *dot; + + *(p++) = '.'; + + strncpy((char *)p, c->dns_search[i].n, + sizeof(buf) - + ((intptr_t)p - (intptr_t)buf)); + for (dot = (char *)p - 1; *dot; dot++) { + if (*dot == '.') + *dot = strcspn(dot + 1, "."); + } + p += strlen(c->dns_search[i].n); + *(p++) = 0; + } + + memset(p, 0, len % 8); /* padding */ + p += len % 8; + } *p++ = 1; /* source ll */ *p++ = 1; /* length */ diff --git a/passt.c b/passt.c index c2c630e..d0dcb26 100644 --- a/passt.c +++ b/passt.c @@ -299,30 +299,49 @@ out: */ static void get_dns(struct ctx *c) { + struct in6_addr *dns6 = &c->dns6[0]; + struct fqdn *s = c->dns_search; + uint32_t *dns4 = &c->dns4[0]; char buf[BUFSIZ], *p, *end; - int dns4 = 0, dns6 = 0; FILE *r; r = fopen("/etc/resolv.conf", "r"); - while (fgets(buf, BUFSIZ, r) && !(dns4 && dns6)) { - if (!strstr(buf, "nameserver ")) - continue; - p = strrchr(buf, ' '); - end = strpbrk(buf, "%\n"); - if (end) - *end = 0; - if (p && inet_pton(AF_INET, p + 1, &c->dns4)) - dns4 = 1; - if (p && inet_pton(AF_INET6, p + 1, &c->dns6)) - dns6 = 1; + while (fgets(buf, BUFSIZ, r)) { + if (strstr(buf, "nameserver ") == buf) { + p = strrchr(buf, ' '); + if (!p) + continue; + + end = strpbrk(buf, "%\n"); + if (end) + *end = 0; + + if (dns4 - &c->dns4[0] < ARRAY_SIZE(c->dns4) && + inet_pton(AF_INET, p + 1, dns4)) + dns4++; + + if (dns6 - &c->dns6[0] < ARRAY_SIZE(c->dns6) && + inet_pton(AF_INET6, p + 1, dns6)) + dns6++; + } else if (strstr(buf, "search ") == buf && + s == c->dns_search) { + end = strpbrk(buf, "\n"); + if (end) + *end = 0; + + p = strtok(buf, " \t"); + while ((p = strtok(NULL, " \t")) && + s - c->dns_search < ARRAY_SIZE(c->dns_search)) { + strncpy(s->n, p, sizeof(c->dns_search[0])); + s++; + } + } } fclose(r); - if (dns4 || dns6) - return; - err("Couldn't get any nameserver address"); - exit(EXIT_FAILURE); + if (dns4 == c->dns4 && dns6 == c->dns6) + warn("Couldn't get any nameserver address"); } /** @@ -785,8 +804,17 @@ int main(int argc, char **argv) inet_ntop(AF_INET, &c.mask4, buf4, sizeof(buf4))); info(" router: %s", inet_ntop(AF_INET, &c.gw4, buf4, sizeof(buf4))); - info(" DNS: %s", - inet_ntop(AF_INET, &c.dns4, buf4, sizeof(buf4))); + for (i = 0; c.dns4[i]; i++) { + if (!i) + info(" DNS:"); + inet_ntop(AF_INET, &c.dns4[i], buf4, sizeof(buf4)); + info(" %s", buf4); + } + for (i = 0; *c.dns_search[i].n; i++) { + if (!i) + info(" search:"); + info(" %s", c.dns_search[i].n); + } } if (c.v6) { info("NDP/DHCPv6:"); @@ -794,8 +822,17 @@ int main(int argc, char **argv) inet_ntop(AF_INET6, &c.addr6, buf6, sizeof(buf6))); info(" router: %s", inet_ntop(AF_INET6, &c.gw6, buf6, sizeof(buf6))); - info(" DNS: %s", - inet_ntop(AF_INET6, &c.dns6, buf6, sizeof(buf6))); + for (i = 0; !IN6_IS_ADDR_UNSPECIFIED(&c.dns6[i]); i++) { + if (!i) + info(" DNS:"); + inet_ntop(AF_INET6, &c.dns6[i], buf6, sizeof(buf6)); + info(" %s", buf6); + } + for (i = 0; *c.dns_search[i].n; i++) { + if (!i) + info(" search:"); + info(" %s", c.dns_search[i].n); + } } listen: diff --git a/passt.h b/passt.h index 22a91d6..bfbdc06 100644 --- a/passt.h +++ b/passt.h @@ -20,6 +20,12 @@ struct tap_msg { #include "tcp.h" #include "udp.h" +#include /* For MAXNS below */ + +struct fqdn { + char n[NS_MAXDNAME]; +}; + /** * struct ctx - Execution context * @epollfd: file descriptor for epoll instance @@ -31,13 +37,14 @@ struct tap_msg { * @addr4_seen: Latest IPv4 address seen as source from tap * @mask4: IPv4 netmask, network order * @gw4: Default IPv4 gateway, network order - * @dns4: IPv4 DNS address, network order + * @dns4: IPv4 DNS addresses, zero-terminated, network order + * @dns_search: DNS search list * @v6: Enable IPv6 transport * @addr6: IPv6 address for external, routable interface * @addr6_seen: Latest IPv6 global/site address seen as source from tap * @addr6_ll_seen: Latest IPv6 link-local address seen as source from tap * @gw6: Default IPv6 gateway - * @dns4: IPv6 DNS address + * @dns4: IPv4 DNS addresses, zero-terminated * @ifn: Name of routable interface */ struct ctx { @@ -51,14 +58,16 @@ struct ctx { uint32_t addr4_seen; uint32_t mask4; uint32_t gw4; - uint32_t dns4; + uint32_t dns4[MAXNS + 1]; + + struct fqdn dns_search[MAXDNSRCH]; int v6; struct in6_addr addr6; struct in6_addr addr6_seen; struct in6_addr addr6_ll_seen; struct in6_addr gw6; - struct in6_addr dns6; + struct in6_addr dns6[MAXNS + 1]; char ifn[IF_NAMESIZE]; diff --git a/util.h b/util.h index fe129ee..7d0704c 100644 --- a/util.h +++ b/util.h @@ -22,11 +22,16 @@ void debug(const char *format, ...); CHECK_SET_MIN_MAX(c->proto_ctx.fd_, (fd)); \ } while (0) +#ifndef MIN #define MIN(x, y) (((x) < (y)) ? (x) : (y)) +#endif +#ifndef MAX #define MAX(x, y) (((x) > (y)) ? (x) : (y)) +#endif + +#define ARRAY_SIZE(a) ((int)(sizeof(a) / sizeof((a)[0]))) #define IN_INTERVAL(a, b, x) ((x) >= (a) && (x) <= (b)) - #define FD_PROTO(x, proto) \ (IN_INTERVAL(c->proto.fd_min, c->proto.fd_max, (x)))