diff options
46 files changed, 2028 insertions, 123 deletions
diff --git a/man/systemd.network.xml b/man/systemd.network.xml index d2a6773883..c12dc68801 100644 --- a/man/systemd.network.xml +++ b/man/systemd.network.xml @@ -2606,6 +2606,18 @@ NFTSet=prefix:netdev:filter:eth_ipv4_prefix</programlisting> </varlistentry> <varlistentry> + <term><varname>UseDNR=</varname></term> + <listitem> + <para>When true, designated resolvers advertised by the DHCP server will be used as encrypted + DNS servers. See <ulink url="https://datatracker.ietf.org/doc/html/rfc9463">RFC 9463</ulink>.</para> + + <para>Defaults to unset, and the value for <varname>UseDNS=</varname> will be used.</para> + + <xi:include href="version-info.xml" xpointer="v257"/> + </listitem> + </varlistentry> + + <varlistentry> <term><varname>UseMTU=</varname></term> <listitem> <para>When true, the interface maximum transmission unit from the DHCP server will be used on @@ -3131,6 +3143,7 @@ NFTSet=prefix:netdev:filter:eth_ipv4_prefix</programlisting> <varlistentry> <term><varname>UseDNS=</varname></term> + <term><varname>UseDNR=</varname></term> <term><varname>UseNTP=</varname></term> <term><varname>UseHostname=</varname></term> <term><varname>UseDomains=</varname></term> @@ -3419,6 +3432,16 @@ Token=prefixstable:2002:da8:1::</programlisting></para> </varlistentry> <varlistentry> + <term><varname>UseDNR=</varname></term> + <listitem> + <para> When true, the DNR servers received in the Router Advertisement will be used. Defaults to + the value of <option>UseDNS=</option>.</para> + + <xi:include href="version-info.xml" xpointer="v257"/> + </listitem> + </varlistentry> + + <varlistentry> <term><varname>UseDomains=</varname></term> <listitem> <para>Takes a boolean, or the special value <literal>route</literal>. When true, the domain name diff --git a/src/libsystemd-network/dhcp-lease-internal.h b/src/libsystemd-network/dhcp-lease-internal.h index b7bc142ef7..394c71bdf0 100644 --- a/src/libsystemd-network/dhcp-lease-internal.h +++ b/src/libsystemd-network/dhcp-lease-internal.h @@ -55,6 +55,9 @@ struct sd_dhcp_lease { DHCPServerData servers[_SD_DHCP_LEASE_SERVER_TYPE_MAX]; + sd_dns_resolver *dnr; + size_t n_dnr; + struct sd_dhcp_route *static_routes; size_t n_static_routes; struct sd_dhcp_route *classless_routes; diff --git a/src/libsystemd-network/dhcp-option.h b/src/libsystemd-network/dhcp-option.h index aaa8f847b1..047c8f3415 100644 --- a/src/libsystemd-network/dhcp-option.h +++ b/src/libsystemd-network/dhcp-option.h @@ -4,6 +4,7 @@ #include <stdint.h> #include "sd-dhcp-option.h" +#include "dns-resolver-internal.h" #include "dhcp-protocol.h" #include "hash-funcs.h" diff --git a/src/libsystemd-network/dhcp6-lease-internal.h b/src/libsystemd-network/dhcp6-lease-internal.h index e76a108f60..60cd84f2d8 100644 --- a/src/libsystemd-network/dhcp6-lease-internal.h +++ b/src/libsystemd-network/dhcp6-lease-internal.h @@ -8,6 +8,7 @@ #include <inttypes.h> #include "sd-dhcp6-lease.h" +#include "dns-resolver-internal.h" #include "dhcp6-option.h" #include "dhcp6-protocol.h" @@ -38,6 +39,8 @@ struct sd_dhcp6_lease { struct in6_addr *dns; size_t dns_count; + sd_dns_resolver *dnr; + size_t n_dnr; char **domains; struct in6_addr *ntp; size_t ntp_count; diff --git a/src/libsystemd-network/dhcp6-option.c b/src/libsystemd-network/dhcp6-option.c index 83f40f3f02..507cd3547d 100644 --- a/src/libsystemd-network/dhcp6-option.c +++ b/src/libsystemd-network/dhcp6-option.c @@ -206,6 +206,7 @@ bool dhcp6_option_can_request(uint16_t option) { case SD_DHCP6_OPTION_V6_DOTS_RI: case SD_DHCP6_OPTION_V6_DOTS_ADDRESS: case SD_DHCP6_OPTION_IPV6_ADDRESS_ANDSF: + case SD_DHCP6_OPTION_V6_DNR: return true; default: return false; @@ -820,74 +821,6 @@ int dhcp6_option_parse_addresses( return 0; } -static int parse_domain(const uint8_t **data, size_t *len, char **ret) { - _cleanup_free_ char *domain = NULL; - const uint8_t *optval; - size_t optlen, n = 0; - int r; - - assert(data); - assert(len); - assert(*data || *len == 0); - assert(ret); - - optval = *data; - optlen = *len; - - if (optlen <= 1) - return -ENODATA; - - for (;;) { - const char *label; - uint8_t c; - - if (optlen == 0) - break; - - c = *optval; - optval++; - optlen--; - - if (c == 0) - /* End label */ - break; - if (c > 63) - return -EBADMSG; - if (c > optlen) - return -EMSGSIZE; - - /* Literal label */ - label = (const char*) optval; - optval += c; - optlen -= c; - - if (!GREEDY_REALLOC(domain, n + (n != 0) + DNS_LABEL_ESCAPED_MAX)) - return -ENOMEM; - - if (n != 0) - domain[n++] = '.'; - - r = dns_label_escape(label, c, domain + n, DNS_LABEL_ESCAPED_MAX); - if (r < 0) - return r; - - n += r; - } - - if (n > 0) { - if (!GREEDY_REALLOC(domain, n + 1)) - return -ENOMEM; - - domain[n] = '\0'; - } - - *ret = TAKE_PTR(domain); - *data = optval; - *len = optlen; - - return n; -} - int dhcp6_option_parse_domainname(const uint8_t *optval, size_t optlen, char **ret) { _cleanup_free_ char *domain = NULL; int r; @@ -895,7 +828,7 @@ int dhcp6_option_parse_domainname(const uint8_t *optval, size_t optlen, char **r assert(optval || optlen == 0); assert(ret); - r = parse_domain(&optval, &optlen, &domain); + r = dns_name_from_wire_format(&optval, &optlen, &domain); if (r < 0) return r; if (r == 0) @@ -922,11 +855,11 @@ int dhcp6_option_parse_domainname_list(const uint8_t *optval, size_t optlen, cha while (optlen > 0) { _cleanup_free_ char *name = NULL; - r = parse_domain(&optval, &optlen, &name); + r = dns_name_from_wire_format(&optval, &optlen, &name); if (r < 0) return r; - if (r == 0) - continue; + if (dns_name_is_root(name)) /* root domain */ + return -EBADMSG; r = strv_consume(&names, TAKE_PTR(name)); if (r < 0) diff --git a/src/libsystemd-network/dns-resolver-internal.h b/src/libsystemd-network/dns-resolver-internal.h new file mode 100644 index 0000000000..e5c185c2e0 --- /dev/null +++ b/src/libsystemd-network/dns-resolver-internal.h @@ -0,0 +1,59 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include <errno.h> + +#include "sd-dns-resolver.h" + +#include "macro.h" +#include "list.h" +#include "socket-netlink.h" + +/* https://www.iana.org/assignments/dns-svcb/dns-svcb.xhtml#dns-svcparamkeys */ +enum { + DNS_SVC_PARAM_KEY_MANDATORY = 0, /* RFC 9460 § 8 */ + DNS_SVC_PARAM_KEY_ALPN = 1, /* RFC 9460 § 7.1 */ + DNS_SVC_PARAM_KEY_NO_DEFAULT_ALPN = 2, /* RFC 9460 § 7.1 */ + DNS_SVC_PARAM_KEY_PORT = 3, /* RFC 9460 § 7.2 */ + DNS_SVC_PARAM_KEY_IPV4HINT = 4, /* RFC 9460 § 7.3 */ + DNS_SVC_PARAM_KEY_ECH = 5, /* RFC 9460 */ + DNS_SVC_PARAM_KEY_IPV6HINT = 6, /* RFC 9460 § 7.3 */ + DNS_SVC_PARAM_KEY_DOHPATH = 7, /* RFC 9461 */ + DNS_SVC_PARAM_KEY_OHTTP = 8, + _DNS_SVC_PARAM_KEY_MAX_DEFINED, + DNS_SVC_PARAM_KEY_INVALID = 65535 /* RFC 9460 */ +}; + +const char* dns_svc_param_key_to_string(int i) _const_; +const char* format_dns_svc_param_key(uint16_t i, char buf[static DECIMAL_STR_MAX(uint16_t)+3]); +#define FORMAT_DNS_SVC_PARAM_KEY(i) format_dns_svc_param_key(i, (char [DECIMAL_STR_MAX(uint16_t)+3]) {}) + +/* Represents a "designated resolver" */ +/* typedef struct sd_dns_resolver sd_dns_resolver; */ +struct sd_dns_resolver { + uint16_t priority; + char *auth_name; + int family; + union in_addr_union *addrs; + size_t n_addrs; + sd_dns_alpn_flags transports; + uint16_t port; + char *dohpath; +}; + +void siphash24_compress_resolver(const sd_dns_resolver *res, struct siphash *state); + +int dns_resolver_transports_to_strv(sd_dns_alpn_flags transports, char ***ret); + +int dns_resolvers_to_dot_addrs(const sd_dns_resolver *resolvers, size_t n_resolvers, + struct in_addr_full ***ret_addrs, size_t *ret_n_addrs); + +int dns_resolver_prio_compare(const sd_dns_resolver *a, const sd_dns_resolver *b); + +int dnr_parse_svc_params(const uint8_t *option, size_t len, sd_dns_resolver *resolver); + +int dns_resolvers_to_dot_strv(const sd_dns_resolver *resolvers, size_t n_resolvers, char ***ret_names); + +void sd_dns_resolver_done(sd_dns_resolver *res); + +void dns_resolver_done_many(sd_dns_resolver *resolvers, size_t n); diff --git a/src/libsystemd-network/meson.build b/src/libsystemd-network/meson.build index 718495cd8e..262ce9c4ef 100644 --- a/src/libsystemd-network/meson.build +++ b/src/libsystemd-network/meson.build @@ -23,6 +23,7 @@ sources = files( 'sd-dhcp-server.c', 'sd-dhcp6-client.c', 'sd-dhcp6-lease.c', + 'sd-dns-resolver.c', 'sd-ipv4acd.c', 'sd-ipv4ll.c', 'sd-lldp-rx.c', diff --git a/src/libsystemd-network/ndisc-option.c b/src/libsystemd-network/ndisc-option.c index af38c32be1..1071d98b19 100644 --- a/src/libsystemd-network/ndisc-option.c +++ b/src/libsystemd-network/ndisc-option.c @@ -3,6 +3,7 @@ #include <linux/ipv6.h> #include <netinet/icmp6.h> +#include "dns-resolver-internal.h" #include "dns-domain.h" #include "ether-addr-util.h" #include "hostname-util.h" @@ -89,6 +90,13 @@ static void ndisc_dnssl_done(sd_ndisc_dnssl *dnssl) { strv_free(dnssl->domains); } +static void ndisc_dnr_done(sd_ndisc_dnr *dnr) { + if (!dnr) + return; + + sd_dns_resolver_unref(dnr->resolver); +} + sd_ndisc_option* ndisc_option_free(sd_ndisc_option *option) { if (!option) return NULL; @@ -109,6 +117,10 @@ sd_ndisc_option* ndisc_option_free(sd_ndisc_option *option) { case SD_NDISC_OPTION_CAPTIVE_PORTAL: free(option->captive_portal); break; + + case SD_NDISC_OPTION_ENCRYPTED_DNS: + ndisc_dnr_done(&option->encrypted_dns); + break; } return mfree(option); @@ -1270,6 +1282,283 @@ static int ndisc_option_build_prefix64(const sd_ndisc_option *option, usec_t tim return 0; } +int ndisc_option_add_encrypted_dns_internal( + Set **options, + size_t offset, + sd_dns_resolver *res, + usec_t lifetime, + usec_t valid_until) { + assert(options); + + sd_ndisc_option *p = ndisc_option_new(SD_NDISC_OPTION_ENCRYPTED_DNS, offset); + if (!p) + return -ENOMEM; + + p->encrypted_dns = (sd_ndisc_dnr) { + .resolver = res, + .lifetime = lifetime, + .valid_until = valid_until, + }; + + return ndisc_option_consume(options, p); +} + +static int ndisc_get_dns_name(const uint8_t *optval, size_t optlen, char **ret) { + _cleanup_free_ char *name = NULL; + int r; + + assert(optval || optlen == 0); + assert(ret); + + r = dns_name_from_wire_format(&optval, &optlen, &name); + if (r < 0) + return r; + if (r == 0 || optlen != 0) + return -EBADMSG; + + *ret = TAKE_PTR(name); + return r; +} + +static int ndisc_option_parse_encrypted_dns(Set **options, size_t offset, size_t len, const uint8_t *opt) { + int r; + + assert(options); + assert(opt); + _cleanup_(sd_dns_resolver_done) sd_dns_resolver res = {}; + usec_t lifetime; + size_t ilen; + + /* Every field up to and including adn must be present */ + if (len < 2*8) + return -EBADMSG; + + if (opt[0] != SD_NDISC_OPTION_ENCRYPTED_DNS) + return -EBADMSG; + + size_t off = 2; + + /* Priority */ + res.priority = unaligned_read_be16(opt + off); + /* Alias mode is not allowed */ + if (res.priority == 0) + return -EBADMSG; + off += sizeof(uint16_t); + + /* Lifetime */ + lifetime = unaligned_be32_sec_to_usec(opt + off, /* max_as_infinity = */ true); + off += sizeof(uint32_t); + + /* adn field (length + dns-name) */ + ilen = unaligned_read_be16(opt + off); + off += sizeof(uint16_t); + if (off + ilen > len) + return -EBADMSG; + + r = ndisc_get_dns_name(opt + off, ilen, &res.auth_name); + if (r < 0) + return r; + if (dns_name_is_root(res.auth_name)) + return -EBADMSG; + off += ilen; + + /* This is the last field in adn-only mode, sans padding */ + if (8 * DIV_ROUND_UP(off, 8) == len && memeqzero(opt + off, len - off)) + return log_debug_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "Received ADN-only encrypted DNS option, ignoring."); + + /* Fields following the variable (octets) length adn field are no longer certain to be aligned. */ + + /* addrs (length + packed struct in6_addr) */ + if (off + sizeof(uint16_t) > len) + return -EBADMSG; + ilen = unaligned_read_be16(opt + off); + off += sizeof(uint16_t); + if (off + ilen > len || ilen % (sizeof(struct in6_addr)) != 0) + return -EBADMSG; + + size_t n_addrs = ilen / (sizeof(struct in6_addr)); + if (n_addrs == 0) + return -EBADMSG; + res.addrs = new(union in_addr_union, n_addrs); + if (!res.addrs) + return -ENOMEM; + + for (size_t i = 0; i < n_addrs; i++) { + union in_addr_union addr; + memcpy(&addr.in6, opt + off, sizeof(struct in6_addr)); + if (in_addr_is_multicast(AF_INET6, &addr) || + in_addr_is_localhost(AF_INET, &addr)) + return -EBADMSG; + res.addrs[i] = addr; + off += sizeof(struct in6_addr); + } + res.n_addrs = n_addrs; + res.family = AF_INET6; + + /* SvcParam field. (length + SvcParams) */ + if (off + sizeof(uint16_t) > len) + return -EBADMSG; + ilen = unaligned_read_be16(opt + off); + off += sizeof(uint16_t); + if (off + ilen > len) + return -EBADMSG; + + r = dnr_parse_svc_params(opt + off, ilen, &res); + if (r < 0) + return r; + if (r == 0) /* This indicates a valid message we don't support */ + return -EOPNOTSUPP; + off += ilen; + + /* the remaining padding bytes must be zeroed */ + if (len - off >= 8 || !memeqzero(opt + off, len - off)) + return -EBADMSG; + + sd_dns_resolver *new_res = new(sd_dns_resolver, 1); + if (!new_res) + return -ENOMEM; + + *new_res = TAKE_STRUCT(res); + + return ndisc_option_add_encrypted_dns(options, offset, new_res, lifetime); +} + +static int ndisc_option_build_encrypted_dns(const sd_ndisc_option *option, usec_t timestamp, uint8_t **ret) { + int r; + + assert(option); + assert(option->type == SD_NDISC_OPTION_ENCRYPTED_DNS); + assert(ret); + + size_t off, len, ilen, plen, poff; + + /* Everything up to adn field is required, so we need at least 2*8 bytes */ + _cleanup_free_ uint8_t *buf = new(uint8_t, 2 * 8); + if (!buf) + return -ENOMEM; + + _cleanup_strv_free_ char **alpns = NULL; + const sd_dns_resolver *res = option->encrypted_dns.resolver; + be32_t lifetime = usec_to_be32_sec(MIN(option->encrypted_dns.lifetime, + usec_sub_unsigned(option->encrypted_dns.valid_until, timestamp))); + + /* Type (Length field filled in last) */ + buf[0] = option->type; + + /* Priority */ + off = 2; + unaligned_write_be16(buf + off, res->priority); + off += sizeof(be16_t); + + /* Lifetime */ + memcpy(buf + off, &lifetime, sizeof(be32_t)); + off += sizeof(be32_t); + + /* ADN */ + //FIXME can the wire format be longer than this? + ilen = strlen(res->auth_name) + 2; + + /* From now on, there isn't guaranteed to be enough space to put each field */ + if (!GREEDY_REALLOC(buf, off + sizeof(uint16_t) + ilen)) + return -ENOMEM; + + r = dns_name_to_wire_format(res->auth_name, buf + off + sizeof(uint16_t), ilen, /* canonical = */ false); + if (r < 0) + return r; + unaligned_write_be16(buf + off, (uint16_t) r); + off += sizeof(uint16_t) + r; + + /* ADN-only mode */ + if (res->n_addrs == 0) + goto padding; + + /* addrs */ + if (size_multiply_overflow(sizeof(struct in6_addr), res->n_addrs)) + return -ENOMEM; + + ilen = res->n_addrs * sizeof(struct in6_addr); + if (!GREEDY_REALLOC(buf, off + sizeof(uint16_t) + ilen)) + return -ENOMEM; + + unaligned_write_be16(buf + off, ilen); + off += sizeof(uint16_t); + + FOREACH_ARRAY(addr, res->addrs, res->n_addrs) { + memcpy(buf + off, &addr->in6, sizeof(struct in6_addr)); + off += sizeof(struct in6_addr); + } + + /* SvcParam, MUST appear in order */ + poff = off + sizeof(uint16_t); + + /* ALPN */ + dns_resolver_transports_to_strv(res->transports, &alpns); + + /* res needs to have at least one valid transport */ + if (strv_isempty(alpns)) + return -EINVAL; + + plen = 0; + STRV_FOREACH(alpn, alpns) + plen += sizeof(uint8_t) + strlen(*alpn); + + if (!GREEDY_REALLOC(buf, poff + 2 * sizeof(uint16_t) + plen)) + return -ENOMEM; + + unaligned_write_be16(buf + poff, (uint16_t) DNS_SVC_PARAM_KEY_ALPN); + poff += sizeof(uint16_t); + unaligned_write_be16(buf + poff, plen); + poff += sizeof(uint16_t); + + STRV_FOREACH(alpn, alpns) { + size_t alen = strlen(*alpn); + buf[poff++] = alen; + memcpy(buf + poff, *alpn, alen); + poff += alen; + } + + /* port */ + if (res->port > 0) { + plen = 2; + if (!GREEDY_REALLOC(buf, poff + 2 * sizeof(uint16_t) + plen)) + return -ENOMEM; + + unaligned_write_be16(buf + poff, (uint16_t) DNS_SVC_PARAM_KEY_PORT); + poff += sizeof(uint16_t); + unaligned_write_be16(buf + poff, plen); + poff += sizeof(uint16_t); + unaligned_write_be16(buf + poff, res->port); + poff += sizeof(uint16_t); + } + + /* dohpath */ + if (res->dohpath) { + plen = strlen(res->dohpath); + if (!GREEDY_REALLOC(buf, poff + 2 * sizeof(uint16_t) + plen)) + return -ENOMEM; + + unaligned_write_be16(buf + poff, (uint16_t) DNS_SVC_PARAM_KEY_DOHPATH); + poff += sizeof(uint16_t); + unaligned_write_be16(buf + poff, plen); + poff += sizeof(uint16_t); + memcpy(buf + poff, res->dohpath, plen); + poff += plen; + } + + unaligned_write_be16(buf + off, LESS_BY(poff, off)); + off = poff; + +padding: + len = DIV_ROUND_UP(off, 8); + if (!GREEDY_REALLOC(buf, 8*len)) + return -ENOMEM; + memzero(buf + off, 8*len - off); + + buf[1] = len; + *ret = TAKE_PTR(buf); + return 0; +} + static int ndisc_option_parse_default(Set **options, size_t offset, size_t len, const uint8_t *opt) { assert(options); assert(opt); @@ -1376,6 +1665,10 @@ int ndisc_parse_options(ICMP6Packet *packet, Set **ret_options) { r = ndisc_option_parse_prefix64(&options, offset, length, opt); break; + case SD_NDISC_OPTION_ENCRYPTED_DNS: + r = ndisc_option_parse_encrypted_dns(&options, offset, length, opt); + break; + default: r = ndisc_option_parse_default(&options, offset, length, opt); } @@ -1487,6 +1780,10 @@ int ndisc_send(int fd, const struct in6_addr *dst, const struct icmp6_hdr *hdr, r = ndisc_option_build_prefix64(option, timestamp, &buf); break; + case SD_NDISC_OPTION_ENCRYPTED_DNS: + r = ndisc_option_build_encrypted_dns(option, timestamp, &buf); + break; + default: continue; } diff --git a/src/libsystemd-network/ndisc-option.h b/src/libsystemd-network/ndisc-option.h index d7bd86147b..5752533e0f 100644 --- a/src/libsystemd-network/ndisc-option.h +++ b/src/libsystemd-network/ndisc-option.h @@ -9,6 +9,7 @@ #include <sys/uio.h> #include "sd-ndisc-protocol.h" +#include "sd-dns-resolver.h" #include "icmp6-packet.h" #include "macro.h" @@ -66,6 +67,12 @@ typedef struct sd_ndisc_prefix64 { usec_t valid_until; } sd_ndisc_prefix64; +typedef struct sd_ndisc_dnr { + sd_dns_resolver *resolver; + usec_t lifetime; + usec_t valid_until; +} sd_ndisc_dnr; + typedef struct sd_ndisc_option { uint8_t type; size_t offset; @@ -83,6 +90,7 @@ typedef struct sd_ndisc_option { sd_ndisc_dnssl dnssl; /* SD_NDISC_OPTION_DNSSL */ char *captive_portal; /* SD_NDISC_OPTION_CAPTIVE_PORTAL */ sd_ndisc_prefix64 prefix64; /* SD_NDISC_OPTION_PREF64 */ + sd_ndisc_dnr encrypted_dns; /* SD_NDISC_OPTION_ENCRYPTED_DNS */ }; } sd_ndisc_option; @@ -327,4 +335,26 @@ static inline int ndisc_option_set_prefix64( return ndisc_option_add_prefix64_internal(options, 0, prefixlen, prefix, lifetime, valid_until); } +int ndisc_option_add_encrypted_dns_internal( + Set **options, + size_t offset, + sd_dns_resolver *res, + usec_t lifetime, + usec_t valid_until); +static inline int ndisc_option_add_encrypted_dns( + Set **options, + size_t offset, + sd_dns_resolver *res, + usec_t lifetime) { + return ndisc_option_add_encrypted_dns_internal(options, offset, res, lifetime, USEC_INFINITY); +} +static inline int ndisc_option_set_encrypted_dns( + Set **options, + size_t offset, + sd_dns_resolver *res, + usec_t lifetime, + usec_t valid_until) { + return ndisc_option_add_encrypted_dns_internal(options, 0, res, lifetime, valid_until); +} + int ndisc_send(int fd, const struct in6_addr *dst, const struct icmp6_hdr *hdr, Set *options, usec_t timestamp); diff --git a/src/libsystemd-network/network-internal.c b/src/libsystemd-network/network-internal.c index a2b88963da..1563655b79 100644 --- a/src/libsystemd-network/network-internal.c +++ b/src/libsystemd-network/network-internal.c @@ -14,6 +14,7 @@ #include "log.h" #include "network-internal.h" #include "parse-util.h" +#include "strv.h" size_t serialize_in_addrs(FILE *f, const struct in_addr *addresses, @@ -123,6 +124,99 @@ int deserialize_in6_addrs(struct in6_addr **ret, const char *string) { return size; } +int serialize_dnr(FILE *f, const sd_dns_resolver *dnr, size_t n_dnr, bool *with_leading_space) { + int r; + + bool _space = false; + if (!with_leading_space) + with_leading_space = &_space; + + int n = 0; + _cleanup_strv_free_ char **names = NULL; + r = dns_resolvers_to_dot_strv(dnr, n_dnr, &names); + if (r < 0) + return r; + if (r > 0) + fputstrv(f, names, NULL, with_leading_space); + n += r; + return n; +} + +static int coalesce_dnr(sd_dns_resolver *dnr, size_t n_dnr, int family, const char *auth_name, + union in_addr_union *addr) { + assert(dnr || n_dnr == 0); + assert(auth_name); + assert(addr); + + /* Look through list of DNR for matching resolvers to add our addr to. Since DoT is assumed, no need + * to compare transports/dohpath/etc. */ + FOREACH_ARRAY(res, dnr, n_dnr) { + if (family == res->family && streq(auth_name, res->auth_name)) { + if (!GREEDY_REALLOC(res->addrs, res->n_addrs + 1)) + return -ENOMEM; + res->addrs[res->n_addrs++] = *addr; + return true; + } + } + + return false; +} + +/* Deserialized resolvers are assumed to offer DoT service. */ +int deserialize_dnr(sd_dns_resolver **ret, const char *string) { + int r; + + assert(ret); + assert(string); + + sd_dns_resolver *dnr = NULL; + size_t n = 0; + CLEANUP_ARRAY(dnr, n, dns_resolver_done_many); + int priority = 0; + + for (;;) { + _cleanup_free_ char *word = NULL; + + r = extract_first_word(&string, &word, NULL, 0); + if (r < 0) + return r; + if (r == 0) + break; + + uint16_t port; + int family; + _cleanup_free_ union in_addr_union *addr = new(union in_addr_union, 1); + _cleanup_free_ char *auth_name = NULL; + + r = in_addr_port_ifindex_name_from_string_auto(word, &family, addr, &port, NULL, &auth_name); + if (r < 0) + return r; + + r = coalesce_dnr(dnr, n, family, auth_name, addr); + if (r < 0) + return r; + if (r > 0) + continue; + + if (!GREEDY_REALLOC(dnr, n+1)) + return -ENOMEM; + + priority = n+1; + dnr[n++] = (sd_dns_resolver) { + .priority = priority, /* not serialized, but this will preserve the order */ + .auth_name = TAKE_PTR(auth_name), + .family = family, + .addrs = TAKE_PTR(addr), + .n_addrs = 1, + .transports = SD_DNS_ALPN_DOT, + .port = port, + }; + } + + *ret = TAKE_PTR(dnr); + return n; +} + void serialize_dhcp_routes(FILE *f, const char *key, sd_dhcp_route **routes, size_t size) { assert(f); assert(key); diff --git a/src/libsystemd-network/network-internal.h b/src/libsystemd-network/network-internal.h index 5aa225e977..6a102e92c4 100644 --- a/src/libsystemd-network/network-internal.h +++ b/src/libsystemd-network/network-internal.h @@ -17,6 +17,9 @@ void serialize_in6_addrs(FILE *f, const struct in6_addr *addresses, bool *with_leading_space); int deserialize_in6_addrs(struct in6_addr **addresses, const char *string); +int serialize_dnr(FILE *f, const sd_dns_resolver *dnr, size_t n_dnr, bool *with_leading_space); +int deserialize_dnr(sd_dns_resolver **ret, const char *string); + /* don't include "dhcp-lease-internal.h" as it causes conflicts between netinet/ip.h and linux/ip.h */ struct sd_dhcp_route; struct sd_dhcp_lease; diff --git a/src/libsystemd-network/sd-dhcp-lease.c b/src/libsystemd-network/sd-dhcp-lease.c index 37f4b3b2c9..1c0cd6829b 100644 --- a/src/libsystemd-network/sd-dhcp-lease.c +++ b/src/libsystemd-network/sd-dhcp-lease.c @@ -11,6 +11,7 @@ #include <unistd.h> #include "sd-dhcp-lease.h" +#include "dns-resolver-internal.h" #include "alloc-util.h" #include "dhcp-lease-internal.h" @@ -26,6 +27,7 @@ #include "network-common.h" #include "network-internal.h" #include "parse-util.h" +#include "sort-util.h" #include "stdio-util.h" #include "string-util.h" #include "strv.h" @@ -229,6 +231,17 @@ int sd_dhcp_lease_get_captive_portal(sd_dhcp_lease *lease, const char **ret) { return 0; } +int sd_dhcp_lease_get_dnr(sd_dhcp_lease *lease, sd_dns_resolver **ret_resolvers) { + assert_return(lease, -EINVAL); + assert_return(ret_resolvers, -EINVAL); + + if (!lease->dnr) + return -ENODATA; + + *ret_resolvers = lease->dnr; + return lease->n_dnr; +} + int sd_dhcp_lease_get_router(sd_dhcp_lease *lease, const struct in_addr **addr) { assert_return(lease, -EINVAL); assert_return(addr, -EINVAL); @@ -418,6 +431,7 @@ static sd_dhcp_lease *dhcp_lease_free(sd_dhcp_lease *lease) { for (sd_dhcp_lease_server_type_t i = 0; i < _SD_DHCP_LEASE_SERVER_TYPE_MAX; i++) free(lease->servers[i].addr); + dns_resolver_done_many(lease->dnr, lease->n_dnr); free(lease->static_routes); free(lease->classless_routes); free(lease->vendor_specific); @@ -559,6 +573,133 @@ static int lease_parse_sip_server(const uint8_t *option, size_t len, struct in_a return lease_parse_in_addrs(option + 1, len - 1, ret, n_ret); } +static int lease_parse_dns_name(const uint8_t *optval, size_t optlen, char **ret) { + _cleanup_free_ char *name = NULL; + int r; + + assert(optval); + assert(ret); + + r = dns_name_from_wire_format(&optval, &optlen, &name); + if (r < 0) + return r; + if (r == 0 || optlen != 0) + return -EBADMSG; + + *ret = TAKE_PTR(name); + return r; +} + +static int lease_parse_dnr(const uint8_t *option, size_t len, sd_dns_resolver **ret_dnr, size_t *ret_n_dnr) { + int r; + sd_dns_resolver *res_list = NULL; + size_t n_resolvers = 0; + CLEANUP_ARRAY(res_list, n_resolvers, dns_resolver_done_many); + + assert(option || len == 0); + assert(ret_dnr); + + _cleanup_(sd_dns_resolver_done) sd_dns_resolver res = {}; + + size_t offset = 0; + while (offset < len) { + /* Instance Data length */ + if (offset + 2 > len) + return -EBADMSG; + size_t ilen = unaligned_read_be16(option + offset); + if (offset + ilen + 2 > len) + return -EBADMSG; + offset += 2; + size_t iend = offset + ilen; + + /* priority */ + if (offset + 2 > len) + return -EBADMSG; + res.priority = unaligned_read_be16(option + offset); + offset += 2; + + /* Authenticated Domain Name */ + if (offset + 1 > len) + return -EBADMSG; + ilen = option[offset++]; + if (offset + ilen > iend) + return -EBADMSG; + + r = lease_parse_dns_name(option + offset, ilen, &res.auth_name); + if (r < 0) + return r; + if (dns_name_is_root(res.auth_name)) + return -EBADMSG; + offset += ilen; + + /* RFC9463 § 3.1.6: In ADN-only mode, server omits everything after the ADN. + * We don't support these, but they are not invalid. */ + if (offset == iend) { + log_debug("Received ADN-only DNRv4 option, ignoring."); + sd_dns_resolver_done(&res); + continue; + } + + /* IPv4 addrs */ + if (offset + 1 > len) + return -EBADMSG; + ilen = option[offset++]; + if (offset + ilen > iend) + return -EBADMSG; + + size_t n_addrs; + _cleanup_free_ struct in_addr *addrs = NULL; + r = lease_parse_in_addrs(option + offset, ilen, &addrs, &n_addrs); + if (r < 0) + return r; + offset += ilen; + + /* RFC9463 § 3.1.8: option MUST include at least one valid IP addr */ + if (!n_addrs) + return -EBADMSG; + + res.addrs = new(union in_addr_union, n_addrs); + if (!res.addrs) + return -ENOMEM; + for (size_t i = 0; i < n_addrs; i++) { + union in_addr_union addr = {.in = addrs[i]}; + /* RFC9463 § 5.2 client MUST discard multicast and host loopback addresses */ + if (in_addr_is_multicast(AF_INET, &addr) || + in_addr_is_localhost(AF_INET, &addr)) + return -EBADMSG; + res.addrs[i] = addr; + } + res.n_addrs = n_addrs; + res.family = AF_INET; + + /* service params */ + r = dnr_parse_svc_params(option + offset, iend-offset, &res); + if (r < 0) + return r; + if (r == 0) { + /* We can't use this record, but it was not invalid. */ + log_debug("Received DNRv4 option with unsupported SvcParams, ignoring."); + sd_dns_resolver_done(&res); + continue; + } + offset = iend; + + /* Append the latest resolver */ + if (!GREEDY_REALLOC0(res_list, n_resolvers+1)) + return -ENOMEM; + + res_list[n_resolvers++] = TAKE_STRUCT(res); + } + + typesafe_qsort(*ret_dnr, *ret_n_dnr, dns_resolver_prio_compare); + + dns_resolver_done_many(*ret_dnr, *ret_n_dnr); + *ret_dnr = TAKE_PTR(res_list); + *ret_n_dnr = n_resolvers; + + return n_resolvers; +} + static int lease_parse_static_routes(sd_dhcp_lease *lease, const uint8_t *option, size_t len) { int r; @@ -879,6 +1020,15 @@ int dhcp_lease_parse_options(uint8_t code, uint8_t len, const void *option, void break; } + case SD_DHCP_OPTION_V4_DNR: + r = lease_parse_dnr(option, len, &lease->dnr, &lease->n_dnr); + if (r < 0) { + log_debug_errno(r, "Failed to parse network-designated resolvers, ignoring: %m"); + return 0; + } + + break; + case SD_DHCP_OPTION_VENDOR_SPECIFIC: if (len <= 0) @@ -1140,6 +1290,14 @@ int dhcp_lease_save(sd_dhcp_lease *lease, const char *lease_file) { fputc('\n', f); } + sd_dns_resolver *resolvers; + r = sd_dhcp_lease_get_dnr(lease, &resolvers); + if (r > 0) { + fputs("DNR=", f); + serialize_dnr(f, resolvers, r, NULL); + fputc('\n', f); + } + r = sd_dhcp_lease_get_ntp(lease, &addresses); if (r > 0) { fputs("NTP=", f); @@ -1248,6 +1406,7 @@ int dhcp_lease_load(sd_dhcp_lease **ret, const char *lease_file) { *next_server = NULL, *broadcast = NULL, *dns = NULL, + *dnr = NULL, *ntp = NULL, *sip = NULL, *pop3 = NULL, @@ -1285,6 +1444,7 @@ int dhcp_lease_load(sd_dhcp_lease **ret, const char *lease_file) { "NEXT_SERVER", &next_server, "BROADCAST", &broadcast, "DNS", &dns, + "DNR", &dnr, "NTP", &ntp, "SIP", &sip, "POP3", &pop3, @@ -1387,6 +1547,13 @@ int dhcp_lease_load(sd_dhcp_lease **ret, const char *lease_file) { lease->servers[SD_DHCP_LEASE_DNS].size = r; } + if (dnr) { + r = deserialize_dnr(&lease->dnr, dnr); + if (r < 0) + log_debug_errno(r, "Failed to deserialize DNR servers %s, ignoring: %m", dnr); + lease->n_dnr = r; + } + if (ntp) { r = deserialize_in_addrs(&lease->servers[SD_DHCP_LEASE_NTP].addr, ntp); if (r < 0) diff --git a/src/libsystemd-network/sd-dhcp6-lease.c b/src/libsystemd-network/sd-dhcp6-lease.c index 261ab83c64..647dea88bd 100644 --- a/src/libsystemd-network/sd-dhcp6-lease.c +++ b/src/libsystemd-network/sd-dhcp6-lease.c @@ -9,6 +9,7 @@ #include "dhcp6-internal.h" #include "dhcp6-lease-internal.h" #include "network-common.h" +#include "sort-util.h" #include "strv.h" #include "unaligned.h" @@ -464,6 +465,98 @@ int sd_dhcp6_lease_get_domains(sd_dhcp6_lease *lease, char ***ret) { return strv_length(lease->domains); } +static int dhcp6_lease_add_dnr(sd_dhcp6_lease *lease, const uint8_t *optval, size_t optlen) { + int r; + + assert(lease); + + _cleanup_(sd_dns_resolver_done) sd_dns_resolver res = {}; + + size_t offset = 0; + + /* priority */ + if (optlen - offset < sizeof(uint16_t)) + return -EBADMSG; + res.priority = unaligned_read_be16(optval + offset); + offset += sizeof(uint16_t); + + /* adn */ + if (optlen - offset < sizeof(uint16_t)) + return -EBADMSG; + size_t ilen = unaligned_read_be16(optval + offset); + offset += sizeof(uint16_t); + if (offset + ilen > optlen) + return -EBADMSG; + + r = dhcp6_option_parse_domainname(optval + offset, ilen, &res.auth_name); + if (r < 0) + return r; + offset += ilen; + + /* RFC9463 § 3.1.6: adn only mode */ + if (offset == optlen) + return 0; + + /* addrs */ + if (optlen - offset < sizeof(uint16_t)) + return -EBADMSG; + ilen = unaligned_read_be16(optval + offset); + offset += sizeof(uint16_t); + if (offset + ilen > optlen) + return -EBADMSG; + + _cleanup_free_ struct in6_addr *addrs = NULL; + size_t n_addrs = 0; + + r = dhcp6_option_parse_addresses(optval + offset, ilen, &addrs, &n_addrs); + if (r < 0) + return r; + if (n_addrs == 0) + return -EBADMSG; + offset += ilen; + + res.addrs = new(union in_addr_union, n_addrs); + if (!res.addrs) + return -ENOMEM; + + for (size_t i = 0; i < n_addrs; i++) { + union in_addr_union addr = {.in6 = addrs[i]}; + /* RFC9463 § 6.2 client MUST discard multicast and host loopback addresses */ + if (in_addr_is_multicast(AF_INET6, &addr) || + in_addr_is_localhost(AF_INET6, &addr)) + return -EBADMSG; + res.addrs[i] = addr; + } + res.n_addrs = n_addrs; + res.family = AF_INET6; + + /* svc params */ + r = dnr_parse_svc_params(optval + offset, optlen-offset, &res); + if (r < 0) + return r; + + /* Append this resolver */ + if (!GREEDY_REALLOC(lease->dnr, lease->n_dnr+1)) + return -ENOMEM; + + lease->dnr[lease->n_dnr++] = TAKE_STRUCT(res); + + typesafe_qsort(lease->dnr, lease->n_dnr, dns_resolver_prio_compare); + + return 1; +} + +int sd_dhcp6_lease_get_dnr(sd_dhcp6_lease *lease, sd_dns_resolver **ret) { + assert_return(lease, -EINVAL); + assert_return(ret, -EINVAL); + + if (!lease->dnr) + return -ENODATA; + + *ret = lease->dnr; + return lease->n_dnr; +} + int dhcp6_lease_add_ntp(sd_dhcp6_lease *lease, const uint8_t *optval, size_t optlen) { int r; @@ -870,6 +963,15 @@ static int dhcp6_lease_parse_message( irt = unaligned_be32_sec_to_usec(optval, /* max_as_infinity = */ false); break; + case SD_DHCP6_OPTION_V6_DNR: + r = dhcp6_lease_add_dnr(lease, optval, optlen); + if (r < 0) + return log_dhcp6_client_errno(client, r, "Failed to parse DNR option, ignoring: %m"); + if (r == 0) + log_dhcp6_client(client, "Received ADN-only DNRv6 option, ignoring."); + + break; + case SD_DHCP6_OPTION_VENDOR_OPTS: r = dhcp6_lease_add_vendor_option(lease, optval, optlen); if (r < 0) @@ -920,6 +1022,7 @@ static sd_dhcp6_lease *dhcp6_lease_free(sd_dhcp6_lease *lease) { dhcp6_ia_free(lease->ia_na); dhcp6_ia_free(lease->ia_pd); free(lease->dns); + dns_resolver_done_many(lease->dnr, lease->n_dnr); free(lease->fqdn); free(lease->captive_portal); strv_free(lease->domains); diff --git a/src/libsystemd-network/sd-dns-resolver.c b/src/libsystemd-network/sd-dns-resolver.c new file mode 100644 index 0000000000..548672cd33 --- /dev/null +++ b/src/libsystemd-network/sd-dns-resolver.c @@ -0,0 +1,388 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "dns-resolver-internal.h" +#include "macro.h" +#include "unaligned.h" +#include "socket-netlink.h" +#include "string-table.h" +#include "string-util.h" +#include "strv.h" + +void sd_dns_resolver_done(sd_dns_resolver *res) { + assert(res); + + res->auth_name = mfree(res->auth_name); + res->addrs = mfree(res->addrs); + res->dohpath = mfree(res->dohpath); +} + +sd_dns_resolver *sd_dns_resolver_unref(sd_dns_resolver *res) { + if (!res) + return NULL; + + sd_dns_resolver_done(res); + return mfree(res); +} + +void dns_resolver_done_many(sd_dns_resolver resolvers[], size_t n) { + assert(resolvers || n == 0); + + FOREACH_ARRAY(res, resolvers, n) + sd_dns_resolver_done(res); + + free(resolvers); +} + +int dns_resolver_prio_compare(const sd_dns_resolver *a, const sd_dns_resolver *b) { + return CMP(ASSERT_PTR(a)->priority, ASSERT_PTR(b)->priority); +} + +int sd_dns_resolver_get_priority(sd_dns_resolver *res, uint16_t *ret_priority) { + assert_return(res, -EINVAL); + assert_return(ret_priority, -EINVAL); + + *ret_priority = res->priority; + return 0; +} + +int sd_dns_resolver_get_adn(sd_dns_resolver *res, const char **ret_adn) { + assert_return(res, -EINVAL); + assert_return(ret_adn, -EINVAL); + + /* Without adn only Do53 can be supported */ + if (!res->auth_name) + return -ENODATA; + + *ret_adn = res->auth_name; + return 0; +} + +int sd_dns_resolver_get_inet_addresses(sd_dns_resolver *res, const struct in_addr **ret_addrs, size_t + *ret_n_addrs) { + assert_return(res, -EINVAL); + assert_return(ret_addrs, -EINVAL); + assert_return(ret_n_addrs, -EINVAL); + assert_return(res->family == AF_INET, -EINVAL); + + /* ADN-only mode has no addrs */ + if (res->n_addrs == 0) + return -ENODATA; + + struct in_addr *addrs = new(struct in_addr, res->n_addrs); + if (!addrs) + return -ENOMEM; + + for (size_t i = 0; i < res->n_addrs; i++) + addrs[i] = res->addrs[i].in; + *ret_addrs = addrs; + *ret_n_addrs = res->n_addrs; + + return 0; +} + +int sd_dns_resolver_get_inet6_addresses(sd_dns_resolver *res, const struct in6_addr **ret_addrs, size_t + *ret_n_addrs) { + assert_return(res, -EINVAL); + assert_return(ret_addrs, -EINVAL); + assert_return(ret_n_addrs, -EINVAL); + assert_return(res->family == AF_INET6, -EINVAL); + + /* ADN-only mode has no addrs */ + if (res->n_addrs == 0) + return -ENODATA; + + struct in6_addr *addrs = new(struct in6_addr, res->n_addrs); + if (!addrs) + return -ENOMEM; + + for (size_t i = 0; i < res->n_addrs; i++) + addrs[i] = res->addrs[i].in6; + *ret_addrs = addrs; + *ret_n_addrs = res->n_addrs; + + return 0; +} + +int sd_dns_resolver_get_alpn(sd_dns_resolver *res, sd_dns_alpn_flags *ret_alpn) { + assert_return(res, -EINVAL); + assert_return(ret_alpn, -EINVAL); + + /* ADN-only mode has no transports */ + if (!res->transports) + return -ENODATA; + + *ret_alpn = res->transports; + return 0; +} + +int sd_dns_resolver_get_port(sd_dns_resolver *res, uint16_t *ret_port) { + assert_return(res, -EINVAL); + assert_return(ret_port, -EINVAL); + + /* port = 0 is the default port */ + *ret_port = res->port; + return 0; +} + +int sd_dns_resolver_get_dohpath(sd_dns_resolver *res, const char **ret_dohpath) { + assert_return(res, -EINVAL); + assert_return(ret_dohpath, -EINVAL); + + /* only present in DoH resolvers */ + if (!res->dohpath) + return -ENODATA; + + *ret_dohpath = res->dohpath; + return 0; +} + +void siphash24_compress_resolver(const sd_dns_resolver *res, struct siphash *state) { + assert(res); + + siphash24_compress_typesafe(res->priority, state); + siphash24_compress_typesafe(res->transports, state); + siphash24_compress_typesafe(res->port, state); + + siphash24_compress_string(res->auth_name, state); + siphash24_compress_string(res->dohpath, state); + + siphash24_compress_typesafe(res->family, state); + FOREACH_ARRAY(addr, res->addrs, res->n_addrs) + siphash24_compress_typesafe(*addr, state); +} + +static const char* const dns_svc_param_key_table[_DNS_SVC_PARAM_KEY_MAX_DEFINED] = { + [DNS_SVC_PARAM_KEY_MANDATORY] = "mandatory", + [DNS_SVC_PARAM_KEY_ALPN] = "alpn", + [DNS_SVC_PARAM_KEY_NO_DEFAULT_ALPN] = "no-default-alpn", + [DNS_SVC_PARAM_KEY_PORT] = "port", + [DNS_SVC_PARAM_KEY_IPV4HINT] = "ipv4hint", + [DNS_SVC_PARAM_KEY_ECH] = "ech", + [DNS_SVC_PARAM_KEY_IPV6HINT] = "ipv6hint", + [DNS_SVC_PARAM_KEY_DOHPATH] = "dohpath", + [DNS_SVC_PARAM_KEY_OHTTP] = "ohttp", +}; +DEFINE_STRING_TABLE_LOOKUP_TO_STRING(dns_svc_param_key, int); + +const char* format_dns_svc_param_key(uint16_t i, char buf[static DECIMAL_STR_MAX(uint16_t)+3]) { + assert(buf); + + const char *p = dns_svc_param_key_to_string(i); + if (p) + return p; + + return snprintf_ok(buf, DECIMAL_STR_MAX(uint16_t)+3, "key%i", i); +} + +int dns_resolver_transports_to_strv(sd_dns_alpn_flags transports, char ***ret) { + _cleanup_strv_free_ char **ans = NULL; + + assert(ret); + + if (FLAGS_SET(transports, SD_DNS_ALPN_DO53)) { + /* Do53 has no ALPN, this flag is only for our own usage. */ + } + + if (FLAGS_SET(transports, SD_DNS_ALPN_HTTP_2_TLS)) + if (strv_extend(&ans, "h2") < 0) + return -ENOMEM; + if (FLAGS_SET(transports, SD_DNS_ALPN_HTTP_3)) + if (strv_extend(&ans, "h3") < 0) + return -ENOMEM; + if (FLAGS_SET(transports, SD_DNS_ALPN_DOT)) + if (strv_extend(&ans, "dot") < 0) + return -ENOMEM; + if (FLAGS_SET(transports, SD_DNS_ALPN_DOQ)) + if (strv_extend(&ans, "doq") < 0) + return -ENOMEM; + + *ret = TAKE_PTR(ans); + return 0; +} + +int dnr_parse_svc_params(const uint8_t *option, size_t len, sd_dns_resolver *resolver) { + size_t offset = 0; + int r; + + assert(option || len == 0); + assert(resolver); + + sd_dns_alpn_flags transports = 0; + uint16_t port = 0; + _cleanup_free_ char *dohpath = NULL; + bool alpn = false; + + uint16_t lastkey = 0; + while (offset < len) { + if (offset + 4 > len) + return -EBADMSG; + + uint16_t key = unaligned_read_be16(&option[offset]); + offset += 2; + + /* RFC9460 § 2.2 SvcParam MUST appear in strictly increasing numeric order */ + if (lastkey >= key) + return -EBADMSG; + lastkey = key; + + uint16_t plen = unaligned_read_be16(&option[offset]); + offset += 2; + if (offset + plen > len) + return -EBADMSG; + + switch (key) { + /* Mandatory keys must be understood by the client, otherwise the record should be discarded. + * Automatic mandatory keys must not appear in the mandatory parameter, so these are all + * supplementary. We don't understand any supplementary keys, so if the mandatory parameter + * is present, we cannot use this record.*/ + case DNS_SVC_PARAM_KEY_MANDATORY: + if (plen > 0) + return -EBADMSG; + break; + + case DNS_SVC_PARAM_KEY_ALPN: + if (plen == 0) + return 0; + alpn = true; /* alpn is required. Record that the requirement is met. */ + + size_t poff = offset; + size_t pend = offset + plen; + while (poff < pend) { + uint8_t alen = option[poff++]; + if (poff + alen > len) + return -EBADMSG; + if (memcmp_nn(&option[poff], alen, "dot", STRLEN("dot")) == 0) + transports |= SD_DNS_ALPN_DOT; + if (memcmp_nn(&option[poff], alen, "h2", STRLEN("h2")) == 0) + transports |= SD_DNS_ALPN_HTTP_2_TLS; + if (memcmp_nn(&option[poff], alen, "h3", STRLEN("h3")) == 0) + transports |= SD_DNS_ALPN_HTTP_3; + if (memcmp_nn(&option[poff], alen, "doq", STRLEN("doq")) == 0) + transports |= SD_DNS_ALPN_DOQ; + poff += alen; + } + if (poff != pend) + return -EBADMSG; + break; + + case DNS_SVC_PARAM_KEY_PORT: + if (plen != sizeof(uint16_t)) + return -EBADMSG; + port = unaligned_read_be16(&option[offset]); + /* Server should indicate default port by omitting this param */ + if (port == 0) + return -EBADMSG; + break; + + /* RFC9463 § 5.1 service params MUST NOT include ipv4hint/ipv6hint */ + case DNS_SVC_PARAM_KEY_IPV4HINT: + case DNS_SVC_PARAM_KEY_IPV6HINT: + return -EBADMSG; + + case DNS_SVC_PARAM_KEY_DOHPATH: + r = make_cstring((const char*) &option[offset], plen, + MAKE_CSTRING_REFUSE_TRAILING_NUL, &dohpath); + if (ERRNO_IS_NEG_RESOURCE(r)) + return r; + if (r < 0) + return -EBADMSG; + /* dohpath is a RFC6750 URI template. We don't parse these, but at least check the + * charset is reasonable. */ + if (!in_charset(dohpath, URI_VALID "{}")) + return -EBADMSG; + break; + + default: + break; + } + offset += plen; + } + if (offset != len) + return -EBADMSG; + + /* DNR cannot be used without alpn */ + if (!alpn) + return -EBADMSG; + + /* RFC9461 § 5: If the [SvcParam] indicates support for HTTP, "dohpath" MUST be present. */ + if (!dohpath && (FLAGS_SET(transports, SD_DNS_ALPN_HTTP_2_TLS) || + FLAGS_SET(transports, SD_DNS_ALPN_HTTP_3))) + return -EBADMSG; + + /* No useful transports */ + if (!transports) + return 0; + + resolver->transports = transports; + resolver->port = port; + free_and_replace(resolver->dohpath, dohpath); + return transports; +} + +int dns_resolvers_to_dot_addrs(const sd_dns_resolver *resolvers, size_t n_resolvers, + struct in_addr_full ***ret_addrs, size_t *ret_n_addrs) { + assert(ret_addrs); + assert(ret_n_addrs); + assert(resolvers || n_resolvers == 0); + + struct in_addr_full **addrs = NULL; + size_t n = 0; + CLEANUP_ARRAY(addrs, n, in_addr_full_array_free); + + FOREACH_ARRAY(res, resolvers, n_resolvers) { + if (!FLAGS_SET(res->transports, SD_DNS_ALPN_DOT)) + continue; + + FOREACH_ARRAY(i, res->addrs, res->n_addrs) { + _cleanup_(in_addr_full_freep) struct in_addr_full *addr = NULL; + int r; + + addr = new0(struct in_addr_full, 1); + if (!addr) + return -ENOMEM; + if (!GREEDY_REALLOC(addrs, n+1)) + return -ENOMEM; + + r = free_and_strdup(&addr->server_name, res->auth_name); + if (r < 0) + return r; + addr->family = res->family; + addr->port = res->port; + addr->address = *i; + + addrs[n++] = TAKE_PTR(addr); + } + } + + *ret_addrs = TAKE_PTR(addrs); + *ret_n_addrs = n; + return n; +} + +int dns_resolvers_to_dot_strv(const sd_dns_resolver *resolvers, size_t n_resolvers, char ***ret_names) { + assert(ret_names); + int r; + + _cleanup_strv_free_ char **names = NULL; + size_t len = 0; + + struct in_addr_full **addrs = NULL; + size_t n = 0; + CLEANUP_ARRAY(addrs, n, in_addr_full_array_free); + + r = dns_resolvers_to_dot_addrs(resolvers, n_resolvers, &addrs, &n); + if (r < 0) + return r; + + FOREACH_ARRAY(addr, addrs, n) { + const char *name = in_addr_full_to_string(*addr); + if (!name) + return -ENOMEM; + r = strv_extend_with_size(&names, &len, name); + if (r < 0) + return r; + } + + *ret_names = TAKE_PTR(names); + return len; +} diff --git a/src/libsystemd-network/sd-dns-resolver.h b/src/libsystemd-network/sd-dns-resolver.h new file mode 100644 index 0000000000..79d00ba9bc --- /dev/null +++ b/src/libsystemd-network/sd-dns-resolver.h @@ -0,0 +1,44 @@ +#ifndef SD_DNS_RESOLVER_H +#define SD_DNS_RESOLVER_H + +#include <errno.h> +#include <netinet/in.h> +#include <stddef.h> +#include <stdint.h> + +#include "_sd-common.h" + +_SD_BEGIN_DECLARATIONS; + +typedef struct sd_dns_resolver sd_dns_resolver; + +/* https://www.iana.org/assignments/tls-extensiontype-values/tls-extensiontype-values.xhtml#alpn-protocol-ids */ +typedef enum sd_dns_alpn_flags { + /* There isn't really an alpn reserved for Do53 service, but designated resolvers may or may not offer + * Do53 service, so we should probably have a flag to represent this capability. Unfortunately DNR + * does not indicate the status to us.*/ + SD_DNS_ALPN_DO53 = 1 << 0, + /* SD_DNS_ALPN_HTTP_1_1, "http/1.1" [RFC9112] */ + SD_DNS_ALPN_HTTP_2_TLS = 1 << 1, /* "h2" [RFC9113] [RFC9461] */ + /* SD_DNS_ALPN_HTTP_2_TCP, "h2c" [RFC9113] */ + SD_DNS_ALPN_HTTP_3 = 1 << 2, /* "h3" [RFC9114] [RFC9461] */ + SD_DNS_ALPN_DOT = 1 << 3, /* "dot" [RFC7858] [RFC9461] */ + SD_DNS_ALPN_DOQ = 1 << 4, /* "doq" [RFC9250] [RFC9461] */ + + _SD_ENUM_FORCE_S64(SD_DNS_ALPN) +} sd_dns_alpn_flags; + +int sd_dns_resolver_get_priority(sd_dns_resolver *res, uint16_t *ret_priority); +int sd_dns_resolver_get_adn(sd_dns_resolver *res, const char **ret_adn); +int sd_dns_resolver_get_inet_addresses(sd_dns_resolver *res, const struct in_addr **ret_addrs, size_t *n); +int sd_dns_resolver_get_inet6_addresses(sd_dns_resolver *res, const struct in6_addr **ret_addrs, size_t *n); +int sd_dns_resolver_get_alpn(sd_dns_resolver *res, sd_dns_alpn_flags *ret_alpn); +int sd_dns_resolver_get_port(sd_dns_resolver *res, uint16_t *ret_port); +int sd_dns_resolver_get_dohpath(sd_dns_resolver *res, const char **ret_dohpath); + +sd_dns_resolver *sd_dns_resolver_unref(sd_dns_resolver *res); +_SD_DEFINE_POINTER_CLEANUP_FUNC(sd_dns_resolver, sd_dns_resolver_unref); + +_SD_END_DECLARATIONS; + +#endif /* SD_DNS_RESOLVER_H */ diff --git a/src/libsystemd-network/sd-ndisc-router.c b/src/libsystemd-network/sd-ndisc-router.c index 9ca737d493..c978547f81 100644 --- a/src/libsystemd-network/sd-ndisc-router.c +++ b/src/libsystemd-network/sd-ndisc-router.c @@ -8,9 +8,12 @@ #include "sd-ndisc.h" #include "alloc-util.h" +#include "dns-domain.h" +#include "dns-resolver-internal.h" #include "ndisc-internal.h" #include "ndisc-router-internal.h" #include "string-table.h" +#include "unaligned.h" static sd_ndisc_router* ndisc_router_free(sd_ndisc_router *rt) { if (!rt) @@ -91,6 +94,7 @@ DEFINE_GET_TIMESTAMP(route_get_lifetime); DEFINE_GET_TIMESTAMP(rdnss_get_lifetime); DEFINE_GET_TIMESTAMP(dnssl_get_lifetime); DEFINE_GET_TIMESTAMP(prefix64_get_lifetime); +DEFINE_GET_TIMESTAMP(encrypted_dns_get_lifetime); int ndisc_router_parse(sd_ndisc *nd, sd_ndisc_router *rt) { const struct nd_router_advert *a; @@ -342,3 +346,6 @@ DEFINE_GETTER(dnssl, SD_NDISC_OPTION_DNSSL, lifetime, uint64_t); DEFINE_GETTER(prefix64, SD_NDISC_OPTION_PREF64, prefixlen, uint8_t); DEFINE_GETTER(prefix64, SD_NDISC_OPTION_PREF64, prefix, struct in6_addr); DEFINE_GETTER(prefix64, SD_NDISC_OPTION_PREF64, lifetime, uint64_t); + +DEFINE_GETTER(encrypted_dns, SD_NDISC_OPTION_ENCRYPTED_DNS, lifetime, uint64_t); +DEFINE_GETTER(encrypted_dns, SD_NDISC_OPTION_ENCRYPTED_DNS, resolver, sd_dns_resolver*); diff --git a/src/libsystemd-network/test-dhcp6-client.c b/src/libsystemd-network/test-dhcp6-client.c index 29cbdc95b4..9f28ec8bec 100644 --- a/src/libsystemd-network/test-dhcp6-client.c +++ b/src/libsystemd-network/test-dhcp6-client.c @@ -165,10 +165,7 @@ TEST(parse_domain) { domain = mfree(domain); data = (uint8_t []) { 4, 't', 'e', 's', 't' }; - assert_se(dhcp6_option_parse_domainname(data, 5, &domain) >= 0); - assert_se(domain); - assert_se(streq(domain, "test")); - domain = mfree(domain); + assert_se(dhcp6_option_parse_domainname(data, 5, &domain) < 0); data = (uint8_t []) { 0 }; assert_se(dhcp6_option_parse_domainname(data, 1, &domain) < 0); @@ -750,8 +747,8 @@ static const uint8_t msg_reply[] = { 0x00, SD_DHCP6_OPTION_DOMAIN, 0x00, 0x0b, 0x03, 'l', 'a', 'b', 0x05, 'i', 'n', 't', 'r', 'a', 0x00, /* Client FQDN */ - 0x00, SD_DHCP6_OPTION_CLIENT_FQDN, 0x00, 0x12, - 0x01, 0x06, 'c', 'l', 'i', 'e', 'n', 't', 0x03, 'l', 'a', 'b', 0x05, 'i', 'n', 't', 'r', 'a', + 0x00, SD_DHCP6_OPTION_CLIENT_FQDN, 0x00, 0x13, + 0x01, 0x06, 'c', 'l', 'i', 'e', 'n', 't', 0x03, 'l', 'a', 'b', 0x05, 'i', 'n', 't', 'r', 'a', 0x00, /* Vendor specific options */ 0x00, SD_DHCP6_OPTION_VENDOR_OPTS, 0x00, 0x09, 0x00, 0x00, 0x00, 0x20, 0x00, 0xf7, 0x00, 0x01, VENDOR_SUBOPTION_BYTES, @@ -832,8 +829,8 @@ static const uint8_t msg_advertise[] = { 0x00, SD_DHCP6_OPTION_DOMAIN, 0x00, 0x0b, 0x03, 'l', 'a', 'b', 0x05, 'i', 'n', 't', 'r', 'a', 0x00, /* Client FQDN */ - 0x00, SD_DHCP6_OPTION_CLIENT_FQDN, 0x00, 0x12, - 0x01, 0x06, 'c', 'l', 'i', 'e', 'n', 't', 0x03, 'l', 'a', 'b', 0x05, 'i', 'n', 't', 'r', 'a', + 0x00, SD_DHCP6_OPTION_CLIENT_FQDN, 0x00, 0x13, + 0x01, 0x06, 'c', 'l', 'i', 'e', 'n', 't', 0x03, 'l', 'a', 'b', 0x05, 'i', 'n', 't', 'r', 'a', 0x00, /* Vendor specific options */ 0x00, SD_DHCP6_OPTION_VENDOR_OPTS, 0x00, 0x09, 0x00, 0x00, 0x00, 0x20, 0x00, 0xf7, 0x00, 0x01, VENDOR_SUBOPTION_BYTES, diff --git a/src/network/networkd-dhcp-common.h b/src/network/networkd-dhcp-common.h index 194930a87c..778378f5ff 100644 --- a/src/network/networkd-dhcp-common.h +++ b/src/network/networkd-dhcp-common.h @@ -5,6 +5,7 @@ #include "conf-parser.h" #include "dhcp-duid-internal.h" +#include "dns-resolver-internal.h" #include "in-addr-util.h" #include "set.h" #include "time-util.h" diff --git a/src/network/networkd-dhcp4.c b/src/network/networkd-dhcp4.c index ecda55e5be..2dd29bca94 100644 --- a/src/network/networkd-dhcp4.c +++ b/src/network/networkd-dhcp4.c @@ -1564,6 +1564,13 @@ static int dhcp4_configure(Link *link) { if (r < 0) return log_link_debug_errno(link, r, "DHCPv4 CLIENT: Failed to set request flag for SIP server: %m"); } + + if (link_get_use_dnr(link, NETWORK_CONFIG_SOURCE_DHCP4)) { + r = sd_dhcp_client_set_request_option(link->dhcp_client, SD_DHCP_OPTION_V4_DNR); + if (r < 0) + return log_link_debug_errno(link, r, "DHCPv4 CLIENT: Failed to set request flag for DNR: %m"); + } + if (link->network->dhcp_use_captive_portal) { r = sd_dhcp_client_set_request_option(link->dhcp_client, SD_DHCP_OPTION_DHCP_CAPTIVE_PORTAL); if (r < 0) diff --git a/src/network/networkd-dhcp6.c b/src/network/networkd-dhcp6.c index f1ecf64205..5294b43240 100644 --- a/src/network/networkd-dhcp6.c +++ b/src/network/networkd-dhcp6.c @@ -641,6 +641,12 @@ static int dhcp6_configure(Link *link) { return log_link_debug_errno(link, r, "DHCPv6 CLIENT: Failed to request domains: %m"); } + if (link_get_use_dnr(link, NETWORK_CONFIG_SOURCE_DHCP6)) { + r = sd_dhcp6_client_set_request_option(client, SD_DHCP6_OPTION_V6_DNR); + if (r < 0) + return log_link_debug_errno(link, r, "DHCPv6 CLIENT: Failed to request DNR: %m"); + } + if (link->network->dhcp6_use_captive_portal > 0) { r = sd_dhcp6_client_set_request_option(client, SD_DHCP6_OPTION_CAPTIVE_PORTAL); if (r < 0) diff --git a/src/network/networkd-dns.c b/src/network/networkd-dns.c index d3da34da83..a8164912dc 100644 --- a/src/network/networkd-dns.c +++ b/src/network/networkd-dns.c @@ -94,6 +94,37 @@ bool link_get_use_dns(Link *link, NetworkConfigSource proto) { return true; } +bool link_get_use_dnr(Link *link, NetworkConfigSource proto) { + int n; + + assert(link); + + if (!link->network) + return false; + + switch (proto) { + case NETWORK_CONFIG_SOURCE_DHCP4: + n = link->network->dhcp_use_dnr; + break; + case NETWORK_CONFIG_SOURCE_DHCP6: + n = link->network->dhcp6_use_dnr; + break; + case NETWORK_CONFIG_SOURCE_NDISC: + n = link->network->ndisc_use_dnr; + break; + default: + assert_not_reached(); + } + + /* If set explicitly, use that */ + if (n >= 0) + return n; + + /* Otherwise, default to the same as the UseDNS setting. After all, + * this is just another way for the server to tell us about DNS configuration. */ + return link_get_use_dns(link, proto); +} + int config_parse_domains( const char *unit, const char *filename, diff --git a/src/network/networkd-dns.h b/src/network/networkd-dns.h index 915cb32fb1..e273d10328 100644 --- a/src/network/networkd-dns.h +++ b/src/network/networkd-dns.h @@ -17,6 +17,7 @@ typedef enum UseDomains { UseDomains link_get_use_domains(Link *link, NetworkConfigSource proto); bool link_get_use_dns(Link *link, NetworkConfigSource proto); +bool link_get_use_dnr(Link *link, NetworkConfigSource proto); const char* use_domains_to_string(UseDomains p) _const_; UseDomains use_domains_from_string(const char *s) _pure_; diff --git a/src/network/networkd-json.c b/src/network/networkd-json.c index 452df9a17f..7ccf653f74 100644 --- a/src/network/networkd-json.c +++ b/src/network/networkd-json.c @@ -503,6 +503,117 @@ static int dns_append_json(Link *link, sd_json_variant **v) { return json_variant_set_field_non_null(v, "DNS", array); } +static int dnr_append_json_one(Link *link, const struct sd_dns_resolver *res, NetworkConfigSource s, const union in_addr_union *p, sd_json_variant **array) { + _cleanup_(sd_json_variant_unrefp) sd_json_variant *addrs_array = NULL; + _cleanup_strv_free_ char **transports = NULL; + int r; + + assert(link); + assert(res); + assert(array); + + FOREACH_ARRAY(addr, res->addrs, res->n_addrs) { + r = sd_json_variant_append_arrayb( + &addrs_array, + JSON_BUILD_IN_ADDR(addr, res->family)); + if (r < 0) + return r; + } + + r = dns_resolver_transports_to_strv(res->transports, &transports); + if (r < 0) + return r; + + //FIXME ifindex? + return sd_json_variant_append_arrayb( + array, + SD_JSON_BUILD_OBJECT( + SD_JSON_BUILD_PAIR_INTEGER("Family", res->family), + SD_JSON_BUILD_PAIR_INTEGER("Priority", res->priority), + JSON_BUILD_PAIR_VARIANT_NON_NULL("Addresses", addrs_array), + JSON_BUILD_PAIR_UNSIGNED_NON_ZERO("Port", res->port), + JSON_BUILD_PAIR_STRING_NON_EMPTY("ServerName", res->auth_name), + JSON_BUILD_PAIR_STRING_NON_EMPTY("DoHPath", res->dohpath), + JSON_BUILD_PAIR_STRV_NON_EMPTY("Transports", transports), + SD_JSON_BUILD_PAIR_STRING("ConfigSource", network_config_source_to_string(s)), + JSON_BUILD_PAIR_IN_ADDR_NON_NULL("ConfigProvider", p, res->family))); +} + +static int dnr_append_json(Link *link, sd_json_variant **v) { + _cleanup_(sd_json_variant_unrefp) sd_json_variant *array = NULL; + int r; + + assert(link); + assert(v); + + if (!link->network) + return 0; + + if (link->dhcp_lease && link_get_use_dnr(link, NETWORK_CONFIG_SOURCE_DHCP4)) { + struct sd_dns_resolver *dnr; + union in_addr_union s; + int n_dnr; + + r = sd_dhcp_lease_get_server_identifier(link->dhcp_lease, &s.in); + if (r < 0) + return r; + + n_dnr = sd_dhcp_lease_get_dnr(link->dhcp_lease, &dnr); + if (n_dnr < 0) + return 0; + + FOREACH_ARRAY(res, dnr, n_dnr) { + r = dnr_append_json_one(link, + res, + NETWORK_CONFIG_SOURCE_DHCP4, + &s, + &array); + if (r < 0) + return r; + } + } + + if (link->dhcp6_lease && link_get_use_dnr(link, NETWORK_CONFIG_SOURCE_DHCP6)) { + struct sd_dns_resolver *dnr; + union in_addr_union s; + int n_dnr; + + r = sd_dhcp6_lease_get_server_address(link->dhcp6_lease, &s.in6); + if (r < 0) + return r; + + n_dnr = sd_dhcp6_lease_get_dnr(link->dhcp6_lease, &dnr); + if (n_dnr < 0) + return 0; + + FOREACH_ARRAY(res, dnr, n_dnr) { + r = dnr_append_json_one(link, + res, + NETWORK_CONFIG_SOURCE_DHCP6, + &s, + &array); + if (r < 0) + return r; + } + } + + if (link_get_use_dnr(link, NETWORK_CONFIG_SOURCE_NDISC)) { + NDiscDNR *a; + + SET_FOREACH(a, link->ndisc_dnr) { + r = dnr_append_json_one(link, + &a->resolver, + NETWORK_CONFIG_SOURCE_NDISC, + &(union in_addr_union) { .in6 = a->router }, + &array); + if (r < 0) + return r; + } + } + + return json_variant_set_field_non_null(v, "DNR", array); +} + static int server_append_json_one_addr(int family, const union in_addr_union *a, NetworkConfigSource s, const union in_addr_union *p, sd_json_variant **array) { assert(IN_SET(family, AF_INET, AF_INET6)); assert(a); @@ -1268,6 +1379,10 @@ int link_build_json(Link *link, sd_json_variant **ret) { if (r < 0) return r; + r = dnr_append_json(link, &v); + if (r < 0) + return r; + r = ntp_append_json(link, &v); if (r < 0) return r; diff --git a/src/network/networkd-link.h b/src/network/networkd-link.h index e86839af8e..89278f3a43 100644 --- a/src/network/networkd-link.h +++ b/src/network/networkd-link.h @@ -170,6 +170,7 @@ typedef struct Link { Set *ndisc_captive_portals; Set *ndisc_pref64; Set *ndisc_redirects; + Set *ndisc_dnr; uint32_t ndisc_mtu; unsigned ndisc_messages; bool ndisc_configured:1; diff --git a/src/network/networkd-ndisc.c b/src/network/networkd-ndisc.c index fc06e5c38b..46f3954725 100644 --- a/src/network/networkd-ndisc.c +++ b/src/network/networkd-ndisc.c @@ -22,6 +22,7 @@ #include "networkd-route.h" #include "networkd-state-file.h" #include "networkd-sysctl.h" +#include "sort-util.h" #include "string-table.h" #include "string-util.h" #include "strv.h" @@ -1826,6 +1827,147 @@ static int ndisc_router_process_pref64(Link *link, sd_ndisc_router *rt) { return 0; } +static NDiscDNR* ndisc_dnr_free(NDiscDNR *x) { + if (!x) + return NULL; + + sd_dns_resolver_done(&x->resolver); + return mfree(x); +} + +DEFINE_TRIVIAL_CLEANUP_FUNC(NDiscDNR*, ndisc_dnr_free); + +static int ndisc_dnr_compare_func(const NDiscDNR *a, const NDiscDNR *b) { + return CMP(a->resolver.priority, b->resolver.priority) || + strcmp_ptr(a->resolver.auth_name, b->resolver.auth_name) || + CMP(a->resolver.transports, b->resolver.transports) || + CMP(a->resolver.port, b->resolver.port) || + strcmp_ptr(a->resolver.dohpath, b->resolver.dohpath) || + CMP(a->resolver.family, b->resolver.family) || + CMP(a->resolver.n_addrs, b->resolver.n_addrs) || + memcmp(a->resolver.addrs, b->resolver.addrs, sizeof(a->resolver.addrs[0]) * a->resolver.n_addrs); +} + +static void ndisc_dnr_hash_func(const NDiscDNR *x, struct siphash *state) { + assert(x); + + siphash24_compress_resolver(&x->resolver, state); +} + +DEFINE_PRIVATE_HASH_OPS_WITH_KEY_DESTRUCTOR( + ndisc_dnr_hash_ops, + NDiscDNR, + ndisc_dnr_hash_func, + ndisc_dnr_compare_func, + ndisc_dnr_free); + +static int sd_dns_resolver_copy(const sd_dns_resolver *a, sd_dns_resolver *b) { + int r; + + assert(a); + assert(b); + + _cleanup_(sd_dns_resolver_done) sd_dns_resolver c = { + .priority = a->priority, + .transports = a->transports, + .port = a->port, + /* .auth_name */ + .family = a->family, + /* .addrs */ + /* .n_addrs */ + /* .dohpath */ + }; + + /* auth_name */ + r = strdup_to(&c.auth_name, a->auth_name); + if (r < 0) + return r; + + /* addrs, n_addrs */ + c.addrs = newdup(union in_addr_union, a->addrs, a->n_addrs); + if (!c.addrs) + return r; + c.n_addrs = a->n_addrs; + + /* dohpath */ + r = strdup_to(&c.dohpath, a->dohpath); + if (r < 0) + return r; + + *b = TAKE_STRUCT(c); + return 0; +} + +static int ndisc_router_process_encrypted_dns(Link *link, sd_ndisc_router *rt) { + int r; + + assert(link); + assert(link->network); + assert(rt); + + struct in6_addr router; + usec_t lifetime_usec; + sd_dns_resolver *res; + _cleanup_(ndisc_dnr_freep) NDiscDNR *new_entry = NULL; + + if (!link_get_use_dnr(link, NETWORK_CONFIG_SOURCE_NDISC)) + return 0; + + r = sd_ndisc_router_get_sender_address(rt, &router); + if (r < 0) + return log_link_warning_errno(link, r, "Failed to get gateway address from RA: %m"); + + r = sd_ndisc_router_encrypted_dns_get_lifetime_timestamp(rt, CLOCK_BOOTTIME, &lifetime_usec); + if (r < 0) + return log_link_warning_errno(link, r, "Failed to get lifetime of RA message: %m"); + + r = sd_ndisc_router_encrypted_dns_get_resolver(rt, &res); + if (r < 0) + return log_link_warning_errno(link, r, "Failed to get encrypted dns resolvers: %m"); + + NDiscDNR *dnr, d = { .resolver = *res }; + if (lifetime_usec == 0) { + dnr = set_remove(link->ndisc_dnr, &d); + if (dnr) { + ndisc_dnr_free(dnr); + link_dirty(link); + } + return 0; + } + + dnr = set_get(link->ndisc_dnr, &d); + if (dnr) { + dnr->router = router; + dnr->lifetime_usec = lifetime_usec; + return 0; + } + + new_entry = new(NDiscDNR, 1); + if (!new_entry) + return log_oom(); + + *new_entry = (NDiscDNR) { + .router = router, + /* .resolver, */ + .lifetime_usec = lifetime_usec, + }; + r = sd_dns_resolver_copy(res, &new_entry->resolver); + if (r < 0) + return log_oom(); + + /* Not sorted by priority */ + r = set_ensure_put(&link->ndisc_dnr, &ndisc_dnr_hash_ops, new_entry); + if (r < 0) + return log_oom(); + + assert(r > 0); + TAKE_PTR(new_entry); + + link_dirty(link); + + return 0; +} + static int ndisc_router_process_options(Link *link, sd_ndisc_router *rt) { size_t n_captive_portal = 0; int r; @@ -1877,6 +2019,9 @@ static int ndisc_router_process_options(Link *link, sd_ndisc_router *rt) { case SD_NDISC_OPTION_PREF64: r = ndisc_router_process_pref64(link, rt); break; + case SD_NDISC_OPTION_ENCRYPTED_DNS: + r = ndisc_router_process_encrypted_dns(link, rt); + break; } if (r < 0 && r != -EBADMSG) return r; @@ -1889,6 +2034,7 @@ static int ndisc_drop_outdated(Link *link, const struct in6_addr *router, usec_t NDiscRDNSS *rdnss; NDiscCaptivePortal *cp; NDiscPREF64 *p64; + NDiscDNR *dnr; Address *address; Route *route; int r, ret = 0; @@ -1987,6 +2133,14 @@ static int ndisc_drop_outdated(Link *link, const struct in6_addr *router, usec_t * the 'updated' flag. */ } + SET_FOREACH(dnr, link->ndisc_dnr) { + if (dnr->lifetime_usec > timestamp_usec) + continue; /* The resolver is still valid */ + + ndisc_dnr_free(set_remove(link->ndisc_dnr, dnr)); + updated = true; + } + if (updated) link_dirty(link); @@ -2014,6 +2168,7 @@ static int ndisc_setup_expire(Link *link) { NDiscDNSSL *dnssl; NDiscRDNSS *rdnss; NDiscPREF64 *p64; + NDiscDNR *dnr; Address *address; Route *route; int r; @@ -2066,6 +2221,9 @@ static int ndisc_setup_expire(Link *link) { SET_FOREACH(p64, link->ndisc_pref64) lifetime_usec = MIN(lifetime_usec, p64->lifetime_usec); + SET_FOREACH(dnr, link->ndisc_dnr) + lifetime_usec = MIN(lifetime_usec, dnr->lifetime_usec); + if (lifetime_usec == USEC_INFINITY) return 0; @@ -2322,6 +2480,14 @@ static int ndisc_neighbor_handle_router_message(Link *link, sd_ndisc_neighbor *n p64->router = current_address; } + NDiscDNR *dnr; + SET_FOREACH(dnr, link->ndisc_dnr) { + if (!in6_addr_equal(&dnr->router, &original_address)) + continue; + + dnr->router = current_address; + } + return 0; } @@ -2505,7 +2671,7 @@ int ndisc_stop(Link *link) { void ndisc_flush(Link *link) { assert(link); - /* Remove all addresses, routes, RDNSS, DNSSL, and Captive Portal entries, without exception. */ + /* Remove all addresses, routes, RDNSS, DNSSL, DNR, and Captive Portal entries, without exception. */ (void) ndisc_drop_outdated(link, /* router = */ NULL, /* timestamp_usec = */ USEC_INFINITY); (void) ndisc_drop_redirect(link, /* router = */ NULL); @@ -2515,6 +2681,7 @@ void ndisc_flush(Link *link) { link->ndisc_captive_portals = set_free(link->ndisc_captive_portals); link->ndisc_pref64 = set_free(link->ndisc_pref64); link->ndisc_redirects = set_free(link->ndisc_redirects); + link->ndisc_dnr = set_free(link->ndisc_dnr); link->ndisc_mtu = 0; } diff --git a/src/network/networkd-ndisc.h b/src/network/networkd-ndisc.h index 6094881ac5..2ee07d3f72 100644 --- a/src/network/networkd-ndisc.h +++ b/src/network/networkd-ndisc.h @@ -2,6 +2,7 @@ #pragma once #include "conf-parser.h" +#include "dns-resolver-internal.h" #include "time-util.h" typedef struct Address Address; @@ -49,6 +50,12 @@ typedef struct NDiscPREF64 { struct in6_addr prefix; } NDiscPREF64; +typedef struct NDiscDNR { + struct in6_addr router; + usec_t lifetime_usec; + sd_dns_resolver resolver; +} NDiscDNR; + static inline char* NDISC_DNSSL_DOMAIN(const NDiscDNSSL *n) { return ((char*) n) + ALIGN(sizeof(NDiscDNSSL)); } diff --git a/src/network/networkd-network-gperf.gperf b/src/network/networkd-network-gperf.gperf index b223ccaccd..50be74e55f 100644 --- a/src/network/networkd-network-gperf.gperf +++ b/src/network/networkd-network-gperf.gperf @@ -228,6 +228,7 @@ NextHop.Group, config_parse_nexthop_section, DHCPv4.RequestAddress, config_parse_in_addr_non_null, AF_INET, offsetof(Network, dhcp_request_address) DHCPv4.ClientIdentifier, config_parse_dhcp_client_identifier, 0, offsetof(Network, dhcp_client_identifier) DHCPv4.UseDNS, config_parse_tristate, 0, offsetof(Network, dhcp_use_dns) +DHCPv4.UseDNR, config_parse_tristate, 0, offsetof(Network, dhcp_use_dnr) DHCPv4.RoutesToDNS, config_parse_bool, 0, offsetof(Network, dhcp_routes_to_dns) DHCPv4.UseNTP, config_parse_tristate, 0, offsetof(Network, dhcp_use_ntp) DHCPv4.RoutesToNTP, config_parse_bool, 0, offsetof(Network, dhcp_routes_to_ntp) @@ -278,6 +279,7 @@ DHCPv4.RapidCommit, config_parse_tristate, DHCPv6.UseAddress, config_parse_bool, 0, offsetof(Network, dhcp6_use_address) DHCPv6.UseDelegatedPrefix, config_parse_bool, 0, offsetof(Network, dhcp6_use_pd_prefix) DHCPv6.UseDNS, config_parse_tristate, 0, offsetof(Network, dhcp6_use_dns) +DHCPv6.UseDNR, config_parse_tristate, 0, offsetof(Network, dhcp6_use_dnr) DHCPv6.UseHostname, config_parse_bool, 0, offsetof(Network, dhcp6_use_hostname) DHCPv6.UseDomains, config_parse_use_domains, 0, offsetof(Network, dhcp6_use_domains) DHCPv6.UseNTP, config_parse_tristate, 0, offsetof(Network, dhcp6_use_ntp) @@ -307,6 +309,7 @@ IPv6AcceptRA.UseAutonomousPrefix, config_parse_bool, IPv6AcceptRA.UseOnLinkPrefix, config_parse_bool, 0, offsetof(Network, ndisc_use_onlink_prefix) IPv6AcceptRA.UsePREF64, config_parse_bool, 0, offsetof(Network, ndisc_use_pref64) IPv6AcceptRA.UseDNS, config_parse_tristate, 0, offsetof(Network, ndisc_use_dns) +IPv6AcceptRA.UseDNR, config_parse_tristate, 0, offsetof(Network, ndisc_use_dnr) IPv6AcceptRA.UseDomains, config_parse_use_domains, 0, offsetof(Network, ndisc_use_domains) IPv6AcceptRA.UseMTU, config_parse_bool, 0, offsetof(Network, ndisc_use_mtu) IPv6AcceptRA.UseHopLimit, config_parse_bool, 0, offsetof(Network, ndisc_use_hop_limit) diff --git a/src/network/networkd-network.c b/src/network/networkd-network.c index 1fff264036..3d399fe876 100644 --- a/src/network/networkd-network.c +++ b/src/network/networkd-network.c @@ -389,6 +389,7 @@ int network_load_one(Manager *manager, OrderedHashmap **networks, const char *fi .dhcp_use_sip = true, .dhcp_use_captive_portal = true, .dhcp_use_dns = -1, + .dhcp_use_dnr = -1, .dhcp_routes_to_dns = true, .dhcp_use_domains = _USE_DOMAINS_INVALID, .dhcp_use_hostname = true, @@ -408,6 +409,7 @@ int network_load_one(Manager *manager, OrderedHashmap **networks, const char *fi .dhcp6_use_address = true, .dhcp6_use_pd_prefix = true, .dhcp6_use_dns = -1, + .dhcp6_use_dnr = -1, .dhcp6_use_domains = _USE_DOMAINS_INVALID, .dhcp6_use_hostname = true, .dhcp6_use_ntp = -1, @@ -483,6 +485,7 @@ int network_load_one(Manager *manager, OrderedHashmap **networks, const char *fi .ndisc = -1, .ndisc_use_redirect = true, .ndisc_use_dns = -1, + .ndisc_use_dnr = -1, .ndisc_use_gateway = true, .ndisc_use_captive_portal = true, .ndisc_use_route_prefix = true, diff --git a/src/network/networkd-network.h b/src/network/networkd-network.h index 66a8328e29..b4ab117928 100644 --- a/src/network/networkd-network.h +++ b/src/network/networkd-network.h @@ -153,6 +153,7 @@ struct Network { int dhcp_ipv6_only_mode; int dhcp_use_rapid_commit; int dhcp_use_dns; + int dhcp_use_dnr; bool dhcp_routes_to_dns; int dhcp_use_ntp; bool dhcp_routes_to_ntp; @@ -185,6 +186,7 @@ struct Network { bool dhcp6_send_hostname; bool dhcp6_send_hostname_set; int dhcp6_use_dns; + int dhcp6_use_dnr; bool dhcp6_use_hostname; int dhcp6_use_ntp; bool dhcp6_use_captive_portal; @@ -343,6 +345,7 @@ struct Network { /* NDisc support */ int ndisc; + int ndisc_use_dnr; bool ndisc_use_redirect; int ndisc_use_dns; bool ndisc_use_gateway; diff --git a/src/network/networkd-state-file.c b/src/network/networkd-state-file.c index 481e2d930b..4cbf6ec92f 100644 --- a/src/network/networkd-state-file.c +++ b/src/network/networkd-state-file.c @@ -5,6 +5,7 @@ #include "alloc-util.h" #include "dns-domain.h" +#include "dns-resolver-internal.h" #include "escape.h" #include "fd-util.h" #include "fileio.h" @@ -113,6 +114,24 @@ static int link_put_dns(Link *link, OrderedSet **s) { } } + if (link->dhcp_lease && link_get_use_dnr(link, NETWORK_CONFIG_SOURCE_DHCP4)) { + sd_dns_resolver *resolvers; + + r = sd_dhcp_lease_get_dnr(link->dhcp_lease, &resolvers); + if (r >= 0) { + struct in_addr_full **dot_servers; + size_t n = 0; + CLEANUP_ARRAY(dot_servers, n, in_addr_full_array_free); + + r = dns_resolvers_to_dot_addrs(resolvers, r, &dot_servers, &n); + if (r < 0) + return r; + r = ordered_set_put_dns_servers(s, link->ifindex, dot_servers, n); + if (r < 0) + return r; + } + } + if (link->dhcp6_lease && link_get_use_dns(link, NETWORK_CONFIG_SOURCE_DHCP6)) { const struct in6_addr *addresses; @@ -124,6 +143,25 @@ static int link_put_dns(Link *link, OrderedSet **s) { } } + if (link->dhcp6_lease && link_get_use_dnr(link, NETWORK_CONFIG_SOURCE_DHCP6)) { + sd_dns_resolver *resolvers; + + r = sd_dhcp6_lease_get_dnr(link->dhcp6_lease, &resolvers); + if (r >= 0 ) { + struct in_addr_full **dot_servers; + size_t n = 0; + CLEANUP_ARRAY(dot_servers, n, in_addr_full_array_free); + + r = dns_resolvers_to_dot_addrs(resolvers, r, &dot_servers, &n); + if (r < 0) + return r; + + r = ordered_set_put_dns_servers(s, link->ifindex, dot_servers, n); + if (r < 0) + return r; + } + } + if (link_get_use_dns(link, NETWORK_CONFIG_SOURCE_NDISC)) { NDiscRDNSS *a; @@ -134,6 +172,24 @@ static int link_put_dns(Link *link, OrderedSet **s) { } } + if (link_get_use_dnr(link, NETWORK_CONFIG_SOURCE_NDISC)) { + NDiscDNR *a; + + SET_FOREACH(a, link->ndisc_dnr) { + struct in_addr_full **dot_servers = NULL; + size_t n = 0; + CLEANUP_ARRAY(dot_servers, n, in_addr_full_array_free); + + r = dns_resolvers_to_dot_addrs(&a->resolver, 1, &dot_servers, &n); + if (r < 0) + return r; + + r = ordered_set_put_dns_servers(s, link->ifindex, dot_servers, n); + if (r < 0) + return r; + } + } + return 0; } @@ -522,6 +578,60 @@ static void serialize_addresses( fputc('\n', f); } +static void serialize_resolvers( + FILE *f, + const char *lvalue, + bool *space, + sd_dhcp_lease *lease, + bool conditional, + sd_dhcp6_lease *lease6, + bool conditional6) { + + bool _space = false; + if (!space) + space = &_space; + + if (lvalue) + fprintf(f, "%s=", lvalue); + + if (lease && conditional) { + sd_dns_resolver *resolvers; + _cleanup_strv_free_ char **names = NULL; + int r; + + r = sd_dhcp_lease_get_dnr(lease, &resolvers); + if (r < 0) + return (void) log_warning_errno(r, "Failed to get DNR from DHCP lease, ignoring."); + + r = dns_resolvers_to_dot_strv(resolvers, r, &names); + if (r < 0) + return (void) log_warning_errno(r, "Failed to get DoT servers from DHCP DNR, ignoring."); + if (r > 0) + fputstrv(f, names, NULL, space); + } + + if (lease6 && conditional6) { + sd_dns_resolver *resolvers; + _cleanup_strv_free_ char **names = NULL; + int r; + + r = sd_dhcp6_lease_get_dnr(lease6, &resolvers); + if (r < 0) + return (void) log_warning_errno(r, "Failed to get DNR from DHCPv6 lease, ignoring."); + + r = dns_resolvers_to_dot_strv(resolvers, r, &names); + if (r < 0) + return (void) log_warning_errno(r, "Failed to get DoT servers from DHCPv6 DNR, ignoring."); + if (r > 0) + fputstrv(f, names, NULL, space); + } + + if (lvalue) + fputc('\n', f); + + return; +} + static void link_save_domains(Link *link, FILE *f, OrderedSet *static_domains, UseDomains use_domains) { bool space = false; const char *p; @@ -665,6 +775,21 @@ static int link_save(Link *link) { space = false; link_save_dns(link, f, link->network->dns, link->network->n_dns, &space); + /* DNR resolvers are not required to provide Do53 service, however resolved doesn't + * know how to handle such a server so for now Do53 service is required, and + * assumed. */ + serialize_resolvers(f, NULL, &space, + link->dhcp_lease, + link_get_use_dnr(link, NETWORK_CONFIG_SOURCE_DHCP4), + link->dhcp6_lease, + link_get_use_dnr(link, NETWORK_CONFIG_SOURCE_DHCP6)); + + if (link_get_use_dnr(link, NETWORK_CONFIG_SOURCE_NDISC)) { + NDiscDNR *dnr; + SET_FOREACH(dnr, link->ndisc_dnr) + serialize_dnr(f, &dnr->resolver, 1, &space); + } + serialize_addresses(f, NULL, &space, NULL, link->dhcp_lease, diff --git a/src/resolve/resolved-dns-packet.c b/src/resolve/resolved-dns-packet.c index c32a1a9a67..f9991d86ab 100644 --- a/src/resolve/resolved-dns-packet.c +++ b/src/resolve/resolved-dns-packet.c @@ -2881,6 +2881,27 @@ size_t dns_packet_size_unfragmented(DnsPacket *p) { return LESS_BY(p->fragsize, udp_header_size(p->family)); } +static const char* const dns_svc_param_key_table[_DNS_SVC_PARAM_KEY_MAX_DEFINED] = { + [DNS_SVC_PARAM_KEY_MANDATORY] = "mandatory", + [DNS_SVC_PARAM_KEY_ALPN] = "alpn", + [DNS_SVC_PARAM_KEY_NO_DEFAULT_ALPN] = "no-default-alpn", + [DNS_SVC_PARAM_KEY_PORT] = "port", + [DNS_SVC_PARAM_KEY_IPV4HINT] = "ipv4hint", + [DNS_SVC_PARAM_KEY_ECH] = "ech", + [DNS_SVC_PARAM_KEY_IPV6HINT] = "ipv6hint", + [DNS_SVC_PARAM_KEY_DOHPATH] = "dohpath", + [DNS_SVC_PARAM_KEY_OHTTP] = "ohttp", +}; +DEFINE_STRING_TABLE_LOOKUP_TO_STRING(dns_svc_param_key, int); + +const char* format_dns_svc_param_key(uint16_t i, char buf[static DECIMAL_STR_MAX(uint16_t)+3]) { + const char *p = dns_svc_param_key_to_string(i); + if (p) + return p; + + return snprintf_ok(buf, DECIMAL_STR_MAX(uint16_t)+3, "key%i", i); +} + static const char* const dns_rcode_table[_DNS_RCODE_MAX_DEFINED] = { [DNS_RCODE_SUCCESS] = "SUCCESS", [DNS_RCODE_FORMERR] = "FORMERR", @@ -2955,27 +2976,6 @@ const char* format_dns_ede_rcode(int i, char buf[static DECIMAL_STR_MAX(int)]) { return snprintf_ok(buf, DECIMAL_STR_MAX(int), "%i", i); } -static const char* const dns_svc_param_key_table[_DNS_SVC_PARAM_KEY_MAX_DEFINED] = { - [DNS_SVC_PARAM_KEY_MANDATORY] = "mandatory", - [DNS_SVC_PARAM_KEY_ALPN] = "alpn", - [DNS_SVC_PARAM_KEY_NO_DEFAULT_ALPN] = "no-default-alpn", - [DNS_SVC_PARAM_KEY_PORT] = "port", - [DNS_SVC_PARAM_KEY_IPV4HINT] = "ipv4hint", - [DNS_SVC_PARAM_KEY_ECH] = "ech", - [DNS_SVC_PARAM_KEY_IPV6HINT] = "ipv6hint", - [DNS_SVC_PARAM_KEY_DOHPATH] = "dohpath", - [DNS_SVC_PARAM_KEY_OHTTP] = "ohttp", -}; -DEFINE_STRING_TABLE_LOOKUP_TO_STRING(dns_svc_param_key, int); - -const char* format_dns_svc_param_key(uint16_t i, char buf[static DECIMAL_STR_MAX(uint16_t)+3]) { - const char *p = dns_svc_param_key_to_string(i); - if (p) - return p; - - return snprintf_ok(buf, DECIMAL_STR_MAX(uint16_t)+3, "key%i", i); -} - static const char* const dns_protocol_table[_DNS_PROTOCOL_MAX] = { [DNS_PROTOCOL_DNS] = "dns", [DNS_PROTOCOL_MDNS] = "mdns", diff --git a/src/resolve/resolved-dns-packet.h b/src/resolve/resolved-dns-packet.h index 437c220b6a..6fe30bca73 100644 --- a/src/resolve/resolved-dns-packet.h +++ b/src/resolve/resolved-dns-packet.h @@ -363,33 +363,33 @@ const char* format_dns_ede_rcode(int i, char buf[static DECIMAL_STR_MAX(int)]); const char* dns_protocol_to_string(DnsProtocol p) _const_; DnsProtocol dns_protocol_from_string(const char *s) _pure_; +#define LLMNR_MULTICAST_IPV4_ADDRESS ((struct in_addr) { .s_addr = htobe32(224U << 24 | 252U) }) +#define LLMNR_MULTICAST_IPV6_ADDRESS ((struct in6_addr) { .s6_addr = { 0xFF, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x03 } }) + +#define MDNS_MULTICAST_IPV4_ADDRESS ((struct in_addr) { .s_addr = htobe32(224U << 24 | 251U) }) +#define MDNS_MULTICAST_IPV6_ADDRESS ((struct in6_addr) { .s6_addr = { 0xFF, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfb } }) + +extern const struct hash_ops dns_packet_hash_ops; + /* https://www.iana.org/assignments/dns-svcb/dns-svcb.xhtml#dns-svcparamkeys */ enum { - DNS_SVC_PARAM_KEY_MANDATORY = 0, /* RFC 9460 section 8 */ - DNS_SVC_PARAM_KEY_ALPN = 1, /* RFC 9460 section 7.1 */ - DNS_SVC_PARAM_KEY_NO_DEFAULT_ALPN = 2, /* RFC 9460 Section 7.1 */ - DNS_SVC_PARAM_KEY_PORT = 3, /* RFC 9460 section 7.2 */ - DNS_SVC_PARAM_KEY_IPV4HINT = 4, /* RFC 9460 section 7.3 */ - DNS_SVC_PARAM_KEY_ECH = 5, /* RFC 9460 */ - DNS_SVC_PARAM_KEY_IPV6HINT = 6, /* RFC 9460 section 7.3 */ - DNS_SVC_PARAM_KEY_DOHPATH = 7, /* RFC 9461 */ - DNS_SVC_PARAM_KEY_OHTTP = 8, + DNS_SVC_PARAM_KEY_MANDATORY = 0, /* RFC 9460 § 8 */ + DNS_SVC_PARAM_KEY_ALPN = 1, /* RFC 9460 § 7.1 */ + DNS_SVC_PARAM_KEY_NO_DEFAULT_ALPN = 2, /* RFC 9460 § 7.1 */ + DNS_SVC_PARAM_KEY_PORT = 3, /* RFC 9460 § 7.2 */ + DNS_SVC_PARAM_KEY_IPV4HINT = 4, /* RFC 9460 § 7.3 */ + DNS_SVC_PARAM_KEY_ECH = 5, /* RFC 9460 */ + DNS_SVC_PARAM_KEY_IPV6HINT = 6, /* RFC 9460 § 7.3 */ + DNS_SVC_PARAM_KEY_DOHPATH = 7, /* RFC 9461 */ + DNS_SVC_PARAM_KEY_OHTTP = 8, _DNS_SVC_PARAM_KEY_MAX_DEFINED, - DNS_SVC_PARAM_KEY_INVALID = 65535 /* RFC 9460 */ + DNS_SVC_PARAM_KEY_INVALID = 65535 /* RFC 9460 */ }; const char* dns_svc_param_key_to_string(int i) _const_; const char* format_dns_svc_param_key(uint16_t i, char buf[static DECIMAL_STR_MAX(uint16_t)+3]); #define FORMAT_DNS_SVC_PARAM_KEY(i) format_dns_svc_param_key(i, (char [DECIMAL_STR_MAX(uint16_t)+3]) {}) -#define LLMNR_MULTICAST_IPV4_ADDRESS ((struct in_addr) { .s_addr = htobe32(224U << 24 | 252U) }) -#define LLMNR_MULTICAST_IPV6_ADDRESS ((struct in6_addr) { .s6_addr = { 0xFF, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x03 } }) - -#define MDNS_MULTICAST_IPV4_ADDRESS ((struct in_addr) { .s_addr = htobe32(224U << 24 | 251U) }) -#define MDNS_MULTICAST_IPV6_ADDRESS ((struct in6_addr) { .s6_addr = { 0xFF, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfb } }) - -extern const struct hash_ops dns_packet_hash_ops; - static inline uint64_t SD_RESOLVED_FLAGS_MAKE( DnsProtocol protocol, int family, diff --git a/src/shared/dns-domain.c b/src/shared/dns-domain.c index 3635167b33..c9963ade93 100644 --- a/src/shared/dns-domain.c +++ b/src/shared/dns-domain.c @@ -905,6 +905,75 @@ int dns_name_to_wire_format(const char *domain, uint8_t *buffer, size_t len, boo return out - buffer; } +/* Decode a domain name according to RFC 1035 Section 3.1, without compression */ +int dns_name_from_wire_format(const uint8_t **data, size_t *len, char **ret) { + _cleanup_free_ char *domain = NULL; + const uint8_t *optval; + size_t optlen, n = 0; + int r; + + assert(data); + assert(len); + assert(*data || *len == 0); + assert(ret); + + optval = *data; + optlen = *len; + + for (;;) { + const char *label; + uint8_t c; + + /* Unterminated name */ + if (optlen == 0) + return -EBADMSG; + + /* RFC 1035 § 3.1 total length of encoded name is limited to 255 octets */ + if (*len - optlen > 255) + return -EMSGSIZE; + + c = *optval; + optval++; + optlen--; + + if (c == 0) + /* End label */ + break; + if (c > DNS_LABEL_MAX) + return -EBADMSG; + if (c > optlen) + return -EMSGSIZE; + + /* Literal label */ + label = (const char*) optval; + optval += c; + optlen -= c; + + if (!GREEDY_REALLOC(domain, n + (n != 0) + DNS_LABEL_ESCAPED_MAX)) + return -ENOMEM; + + if (n != 0) + domain[n++] = '.'; + + r = dns_label_escape(label, c, domain + n, DNS_LABEL_ESCAPED_MAX); + if (r < 0) + return r; + + n += r; + } + + if (!GREEDY_REALLOC(domain, n + 1)) + return -ENOMEM; + + domain[n] = '\0'; + + *ret = TAKE_PTR(domain); + *data = optval; + *len = optlen; + + return n; +} + static bool srv_type_label_is_valid(const char *label, size_t n) { assert(label); diff --git a/src/shared/dns-domain.h b/src/shared/dns-domain.h index 8ad00d6e4b..edfb2f00ca 100644 --- a/src/shared/dns-domain.h +++ b/src/shared/dns-domain.h @@ -79,6 +79,7 @@ bool dns_name_is_root(const char *name); bool dns_name_is_single_label(const char *name); int dns_name_to_wire_format(const char *domain, uint8_t *buffer, size_t len, bool canonical); +int dns_name_from_wire_format(const uint8_t **data, size_t *len, char **ret); bool dns_srv_type_is_valid(const char *name); bool dnssd_srv_type_is_valid(const char *name); diff --git a/src/shared/socket-netlink.c b/src/shared/socket-netlink.c index 2580c84ead..09477449b9 100644 --- a/src/shared/socket-netlink.c +++ b/src/shared/socket-netlink.c @@ -347,6 +347,15 @@ struct in_addr_full *in_addr_full_free(struct in_addr_full *a) { return mfree(a); } +void in_addr_full_array_free(struct in_addr_full *addrs[], size_t n) { + assert(addrs || n == 0); + + FOREACH_ARRAY(a, addrs, n) + in_addr_full_freep(a); + + free(addrs); +} + int in_addr_full_new( int family, const union in_addr_union *a, diff --git a/src/shared/socket-netlink.h b/src/shared/socket-netlink.h index a6edb4c4bd..12647b3d14 100644 --- a/src/shared/socket-netlink.h +++ b/src/shared/socket-netlink.h @@ -39,6 +39,7 @@ struct in_addr_full { struct in_addr_full *in_addr_full_free(struct in_addr_full *a); DEFINE_TRIVIAL_CLEANUP_FUNC(struct in_addr_full*, in_addr_full_free); +void in_addr_full_array_free(struct in_addr_full *addrs[], size_t n); int in_addr_full_new(int family, const union in_addr_union *a, uint16_t port, int ifindex, const char *server_name, struct in_addr_full **ret); int in_addr_full_new_from_string(const char *s, struct in_addr_full **ret); const char* in_addr_full_to_string(struct in_addr_full *a); diff --git a/src/systemd/sd-dhcp-lease.h b/src/systemd/sd-dhcp-lease.h index eb5970e405..2265b8b9aa 100644 --- a/src/systemd/sd-dhcp-lease.h +++ b/src/systemd/sd-dhcp-lease.h @@ -32,6 +32,7 @@ _SD_BEGIN_DECLARATIONS; typedef struct sd_dhcp_lease sd_dhcp_lease; typedef struct sd_dhcp_route sd_dhcp_route; +typedef struct sd_dns_resolver sd_dns_resolver; sd_dhcp_lease *sd_dhcp_lease_ref(sd_dhcp_lease *lease); sd_dhcp_lease *sd_dhcp_lease_unref(sd_dhcp_lease *lease); @@ -75,6 +76,7 @@ int sd_dhcp_lease_get_search_domains(sd_dhcp_lease *lease, char ***domains); int sd_dhcp_lease_get_hostname(sd_dhcp_lease *lease, const char **hostname); int sd_dhcp_lease_get_root_path(sd_dhcp_lease *lease, const char **root_path); int sd_dhcp_lease_get_captive_portal(sd_dhcp_lease *lease, const char **captive_portal); +int sd_dhcp_lease_get_dnr(sd_dhcp_lease *lease, sd_dns_resolver **ret_resolvers); int sd_dhcp_lease_get_static_routes(sd_dhcp_lease *lease, sd_dhcp_route ***ret); int sd_dhcp_lease_get_classless_routes(sd_dhcp_lease *lease, sd_dhcp_route ***ret); int sd_dhcp_lease_get_vendor_specific(sd_dhcp_lease *lease, const void **data, size_t *data_len); diff --git a/src/systemd/sd-dhcp-protocol.h b/src/systemd/sd-dhcp-protocol.h index d8b7537da2..c25498502d 100644 --- a/src/systemd/sd-dhcp-protocol.h +++ b/src/systemd/sd-dhcp-protocol.h @@ -171,7 +171,8 @@ enum { SD_DHCP_OPTION_PORT_PARAMS = 159, /* [RFC7618] */ /* option code 160 is unassigned [RFC7710][RFC8910] */ SD_DHCP_OPTION_MUD_URL = 161, /* [RFC8520] */ - /* option codes 162-174 are unassigned [RFC3942] */ + SD_DHCP_OPTION_V4_DNR = 162, /* [RFC9463] */ + /* option codes 163-174 are unassigned [RFC3942] */ /* option codes 175-177 are temporary assigned. */ /* option codes 178-207 are unassigned [RFC3942] */ SD_DHCP_OPTION_PXELINUX_MAGIC = 208, /* [RFC5071] Deprecated */ diff --git a/src/systemd/sd-dhcp6-lease.h b/src/systemd/sd-dhcp6-lease.h index e18d57817f..d6bcceb2a2 100644 --- a/src/systemd/sd-dhcp6-lease.h +++ b/src/systemd/sd-dhcp6-lease.h @@ -30,6 +30,7 @@ _SD_BEGIN_DECLARATIONS; typedef struct sd_dhcp6_lease sd_dhcp6_lease; +typedef struct sd_dns_resolver sd_dns_resolver; int sd_dhcp6_lease_get_timestamp(sd_dhcp6_lease *lease, clockid_t clock, uint64_t *ret); int sd_dhcp6_lease_get_t1(sd_dhcp6_lease *lease, uint64_t *ret); @@ -74,6 +75,7 @@ int sd_dhcp6_lease_get_pd_lifetime_timestamp( int sd_dhcp6_lease_has_pd_prefix(sd_dhcp6_lease *lease); int sd_dhcp6_lease_get_dns(sd_dhcp6_lease *lease, const struct in6_addr **ret); +int sd_dhcp6_lease_get_dnr(sd_dhcp6_lease *lease, sd_dns_resolver **ret); int sd_dhcp6_lease_get_domains(sd_dhcp6_lease *lease, char ***ret); int sd_dhcp6_lease_get_ntp_addrs(sd_dhcp6_lease *lease, const struct in6_addr **ret); int sd_dhcp6_lease_get_ntp_fqdn(sd_dhcp6_lease *lease, char ***ret); diff --git a/src/systemd/sd-dhcp6-protocol.h b/src/systemd/sd-dhcp6-protocol.h index 78c80f7c7e..b1d9ca1ffa 100644 --- a/src/systemd/sd-dhcp6-protocol.h +++ b/src/systemd/sd-dhcp6-protocol.h @@ -165,8 +165,9 @@ enum { SD_DHCP6_OPTION_SLAP_QUAD = 140, /* RFC 8948 */ SD_DHCP6_OPTION_V6_DOTS_RI = 141, /* RFC 8973 */ SD_DHCP6_OPTION_V6_DOTS_ADDRESS = 142, /* RFC 8973 */ - SD_DHCP6_OPTION_IPV6_ADDRESS_ANDSF = 143 /* RFC 6153 */ - /* option codes 144-65535 are unassigned */ + SD_DHCP6_OPTION_IPV6_ADDRESS_ANDSF = 143, /* RFC 6153 */ + SD_DHCP6_OPTION_V6_DNR = 144 /* RFC 9463 */ + /* option codes 145-65535 are unassigned */ }; _SD_END_DECLARATIONS; diff --git a/src/systemd/sd-ndisc-router.h b/src/systemd/sd-ndisc-router.h index b07fefba3b..a7f1365381 100644 --- a/src/systemd/sd-ndisc-router.h +++ b/src/systemd/sd-ndisc-router.h @@ -28,6 +28,7 @@ _SD_BEGIN_DECLARATIONS; typedef struct sd_ndisc_router sd_ndisc_router; +typedef struct sd_dns_resolver sd_dns_resolver; sd_ndisc_router *sd_ndisc_router_ref(sd_ndisc_router *rt); sd_ndisc_router *sd_ndisc_router_unref(sd_ndisc_router *rt); @@ -87,6 +88,11 @@ int sd_ndisc_router_prefix64_get_prefixlen(sd_ndisc_router *rt, uint8_t *ret); int sd_ndisc_router_prefix64_get_lifetime(sd_ndisc_router *rt, uint64_t *ret); int sd_ndisc_router_prefix64_get_lifetime_timestamp(sd_ndisc_router *rt, clockid_t clock, uint64_t *ret); +/* Specific option access: SD_NDISC_OPTION_ENCRYPTED_DNS */ +int sd_ndisc_router_encrypted_dns_get_resolver(sd_ndisc_router *rt, sd_dns_resolver **ret); +int sd_ndisc_router_encrypted_dns_get_lifetime(sd_ndisc_router *rt, uint64_t *ret); +int sd_ndisc_router_encrypted_dns_get_lifetime_timestamp(sd_ndisc_router *rt, clockid_t clock, uint64_t *ret); + _SD_END_DECLARATIONS; #endif diff --git a/src/test/test-dns-domain.c b/src/test/test-dns-domain.c index 8088055295..053a93663c 100644 --- a/src/test/test-dns-domain.c +++ b/src/test/test-dns-domain.c @@ -119,6 +119,95 @@ TEST(dns_name_to_wire_format) { test_dns_name_to_wire_format_one("a12345678.a12345678.a12345678.a12345678.a12345678.a12345678.a12345678.a12345678.a12345678.a12345678.a12345678.a12345678.a12345678.a12345678.a12345678.a12345678.a12345678.a12345678.a12345678.a12345678.a12345678.a12345678.a12345678.a12345678.a12345678.a12", out4, sizeof(out4), sizeof(out4)); } +static void test_dns_name_from_wire_format_one(const char *expect, const uint8_t *what, size_t len, int ret) { + _cleanup_free_ char *name = NULL; + int r; + + log_info("%s, %s, %zu, →%d", what, strnull(expect), len, ret); + + r = dns_name_from_wire_format(&what, &len, &name); + assert_se(r == ret); + + if (r >= 0) { + assert(expect); /* for gcc */ + assert_se(memcmp(name, expect, r) == 0); + } +} + +TEST(dns_name_from_wire_format) { + static const uint8_t in0[] = { 0 }; + static const uint8_t in1[] = { 3, 'f', 'o', 'o', 0 }; + static const uint8_t in2[] = { 5, 'h', 'a', 'l', 'l', 'o', 3, 'f', 'o', 'o', 3, 'b', 'a', 'r', 0 }; + static const uint8_t in2_1[] = { 5, 'h', 'a', 'l', 'l', 'o', 3, 'f', 'o', 'o', 0, 'b', 'a', 'r', 0 }; + static const uint8_t in3[] = { 4, ' ', 'f', 'o', 'o', 3, 'b', 'a', 'r', 0 }; + static const uint8_t in4[] = { 9, 'a', '1', '2', '3', '4', '5', '6', '7', '8', + 9, 'a', '1', '2', '3', '4', '5', '6', '7', '8', + 9, 'a', '1', '2', '3', '4', '5', '6', '7', '8', + 9, 'a', '1', '2', '3', '4', '5', '6', '7', '8', + 9, 'a', '1', '2', '3', '4', '5', '6', '7', '8', + 9, 'a', '1', '2', '3', '4', '5', '6', '7', '8', + 9, 'a', '1', '2', '3', '4', '5', '6', '7', '8', + 9, 'a', '1', '2', '3', '4', '5', '6', '7', '8', + 9, 'a', '1', '2', '3', '4', '5', '6', '7', '8', + 9, 'a', '1', '2', '3', '4', '5', '6', '7', '8', + 9, 'a', '1', '2', '3', '4', '5', '6', '7', '8', + 9, 'a', '1', '2', '3', '4', '5', '6', '7', '8', + 9, 'a', '1', '2', '3', '4', '5', '6', '7', '8', + 9, 'a', '1', '2', '3', '4', '5', '6', '7', '8', + 9, 'a', '1', '2', '3', '4', '5', '6', '7', '8', + 9, 'a', '1', '2', '3', '4', '5', '6', '7', '8', + 9, 'a', '1', '2', '3', '4', '5', '6', '7', '8', + 9, 'a', '1', '2', '3', '4', '5', '6', '7', '8', + 9, 'a', '1', '2', '3', '4', '5', '6', '7', '8', + 9, 'a', '1', '2', '3', '4', '5', '6', '7', '8', + 9, 'a', '1', '2', '3', '4', '5', '6', '7', '8', + 9, 'a', '1', '2', '3', '4', '5', '6', '7', '8', + 9, 'a', '1', '2', '3', '4', '5', '6', '7', '8', + 9, 'a', '1', '2', '3', '4', '5', '6', '7', '8', + 9, 'a', '1', '2', '3', '4', '5', '6', '7', '8', + 3, 'a', '1', '2', 0 }; /* 255 octets */ + static const uint8_t in5[] = { 9, 'a', '1', '2', '3', '4', '5', '6', '7', '8', + 9, 'a', '1', '2', '3', '4', '5', '6', '7', '8', + 9, 'a', '1', '2', '3', '4', '5', '6', '7', '8', + 9, 'a', '1', '2', '3', '4', '5', '6', '7', '8', + 9, 'a', '1', '2', '3', '4', '5', '6', '7', '8', + 9, 'a', '1', '2', '3', '4', '5', '6', '7', '8', + 9, 'a', '1', '2', '3', '4', '5', '6', '7', '8', + 9, 'a', '1', '2', '3', '4', '5', '6', '7', '8', + 9, 'a', '1', '2', '3', '4', '5', '6', '7', '8', + 9, 'a', '1', '2', '3', '4', '5', '6', '7', '8', + 9, 'a', '1', '2', '3', '4', '5', '6', '7', '8', + 9, 'a', '1', '2', '3', '4', '5', '6', '7', '8', + 9, 'a', '1', '2', '3', '4', '5', '6', '7', '8', + 9, 'a', '1', '2', '3', '4', '5', '6', '7', '8', + 9, 'a', '1', '2', '3', '4', '5', '6', '7', '8', + 9, 'a', '1', '2', '3', '4', '5', '6', '7', '8', + 9, 'a', '1', '2', '3', '4', '5', '6', '7', '8', + 9, 'a', '1', '2', '3', '4', '5', '6', '7', '8', + 9, 'a', '1', '2', '3', '4', '5', '6', '7', '8', + 9, 'a', '1', '2', '3', '4', '5', '6', '7', '8', + 9, 'a', '1', '2', '3', '4', '5', '6', '7', '8', + 9, 'a', '1', '2', '3', '4', '5', '6', '7', '8', + 9, 'a', '1', '2', '3', '4', '5', '6', '7', '8', + 9, 'a', '1', '2', '3', '4', '5', '6', '7', '8', + 9, 'a', '1', '2', '3', '4', '5', '6', '7', '8', + 9, 'a', '1', '2', '3', '4', '5', '6', '7', '8', + 3, 'a', '1', '2', 0 }; /* 265 octets */ + + test_dns_name_from_wire_format_one("", in0, sizeof(in0), strlen("")); + + test_dns_name_from_wire_format_one("foo", in1, sizeof(in1), strlen("foo")); + test_dns_name_from_wire_format_one(NULL, in1, sizeof(in1) - 1, -EBADMSG); + + test_dns_name_from_wire_format_one("hallo.foo.bar", in2, sizeof(in2), strlen("hallo.foo.bar")); + test_dns_name_from_wire_format_one("hallo.foo", in2_1, sizeof(in2_1), strlen("hallo.foo")); + + test_dns_name_from_wire_format_one("\\032foo.bar", in3, sizeof(in3), strlen("\\032foo.bar")); + + test_dns_name_from_wire_format_one(NULL, in5, sizeof(in5), -EMSGSIZE); + test_dns_name_from_wire_format_one("a12345678.a12345678.a12345678.a12345678.a12345678.a12345678.a12345678.a12345678.a12345678.a12345678.a12345678.a12345678.a12345678.a12345678.a12345678.a12345678.a12345678.a12345678.a12345678.a12345678.a12345678.a12345678.a12345678.a12345678.a12345678.a12", in4, sizeof(in4), 253); +} + static void test_dns_label_unescape_suffix_one(const char *what, const char *expect1, const char *expect2, size_t buffer_sz, int ret1, int ret2) { char buffer[buffer_sz]; const char *label; diff --git a/test/fuzz/fuzz-dhcp-client/dnr_v4 b/test/fuzz/fuzz-dhcp-client/dnr_v4 Binary files differnew file mode 100644 index 0000000000..2294736277 --- /dev/null +++ b/test/fuzz/fuzz-dhcp-client/dnr_v4 diff --git a/test/fuzz/fuzz-dhcp6-client/dnr_v6 b/test/fuzz/fuzz-dhcp6-client/dnr_v6 Binary files differnew file mode 100644 index 0000000000..142de34289 --- /dev/null +++ b/test/fuzz/fuzz-dhcp6-client/dnr_v6 diff --git a/test/fuzz/fuzz-ndisc-rs/encrypted-dns b/test/fuzz/fuzz-ndisc-rs/encrypted-dns Binary files differnew file mode 100644 index 0000000000..59240efcc8 --- /dev/null +++ b/test/fuzz/fuzz-ndisc-rs/encrypted-dns diff --git a/test/test-network/systemd-networkd-tests.py b/test/test-network/systemd-networkd-tests.py index 687fda7b3d..b193f86da9 100755 --- a/test/test-network/systemd-networkd-tests.py +++ b/test/test-network/systemd-networkd-tests.py @@ -17,8 +17,10 @@ import argparse import datetime +import enum import errno import itertools +import ipaddress import json import os import pathlib @@ -27,6 +29,7 @@ import re import shutil import signal import socket +import struct import subprocess import sys import time @@ -742,6 +745,54 @@ def stop_by_pid_file(pid_file): print(f"Unexpected exception when waiting for {pid} to die: {e.errno}") rm_f(pid_file) +def dnr_v4_instance_data(adn, addrs=None, prio=1, alpns=("dot",), dohpath=None): + b = bytes() + pack = lambda c, w=1: struct.pack('>' + "_BH_I"[w], len(c)) + c + pyton = lambda n, w=2: struct.pack('>' + "_BH_I"[w], n) + ipv4 = ipaddress.IPv4Address + class SvcParam(enum.Enum): + ALPN = 1 + DOHPATH = 7 + + data = pyton(prio) + + adn = adn.rstrip('.') + '.' + data += pack(b.join(pack(label.encode('ascii')) for label in adn.split('.'))) + + if not addrs: # adn-only mode + return pack(data, 2) + + data += pack(b.join(ipv4(addr).packed for addr in addrs)) + data += pyton(SvcParam.ALPN.value) + pack(b.join(pack(alpn.encode('ascii')) for alpn in alpns), 2) + if dohpath is not None: + data += pyton(SvcParam.DOHPATH.value) + pack(dohpath.encode('utf-8'), 2) + + return pack(data, 2) + +def dnr_v6_instance_data(adn, addrs=None, prio=1, alpns=("dot",), dohpath=None): + b = bytes() + pack = lambda c, w=1: struct.pack('>' + "_BH_I"[w], len(c)) + c + pyton = lambda n, w=2: struct.pack('>' + "_BH_I"[w], n) + ipv6 = ipaddress.IPv6Address + class SvcParam(enum.Enum): + ALPN = 1 + DOHPATH = 7 + + data = pyton(prio) + + adn = adn.rstrip('.') + '.' + data += pack(b.join(pack(label.encode('ascii')) for label in adn.split('.')), 2) + + if not addrs: # adn-only mode + return data + + data += pack(b.join(ipv6(addr).packed for addr in addrs), 2) + data += pyton(SvcParam.ALPN.value) + pack(b.join(pack(alpn.encode('ascii')) for alpn in alpns), 2) + if dohpath is not None: + data += pyton(SvcParam.DOHPATH.value) + pack(dohpath.encode('utf-8'), 2) + + return data + def start_dnsmasq(*additional_options, interface='veth-peer', ra_mode=None, ipv4_range='192.168.5.10,192.168.5.200', ipv4_router='192.168.5.1', ipv6_range='2600::10,2600::20'): if ra_mode: ra_mode = f',{ra_mode}' @@ -7253,6 +7304,56 @@ class NetworkdDHCPClientTests(unittest.TestCase, Utilities): check(self, False, False, True) check(self, False, False, False) + def test_dhcp_client_use_dnr(self): + def check(self, ipv4, ipv6): + os.makedirs(os.path.join(network_unit_dir, '25-dhcp-client.network.d'), exist_ok=True) + with open(os.path.join(network_unit_dir, '25-dhcp-client.network.d/override.conf'), mode='w', encoding='utf-8') as f: + f.write('[DHCPv4]\nUseDNS=') + f.write('yes' if ipv4 else 'no') + f.write('\n[DHCPv6]\nUseDNS=') + f.write('yes' if ipv6 else 'no') + f.write('\n[IPv6AcceptRA]\nUseDNS=no') + + networkctl_reload() + self.wait_online('veth99:routable') + + # link becomes 'routable' when at least one protocol provide an valid address. Hence, we need to explicitly wait for both addresses. + self.wait_address('veth99', r'inet 192.168.5.[0-9]*/24 metric 1024 brd 192.168.5.255 scope global dynamic', ipv='-4') + self.wait_address('veth99', r'inet6 2600::[0-9a-f]*/128 scope global (dynamic noprefixroute|noprefixroute dynamic)', ipv='-6') + + # make resolved re-read the link state file + resolvectl('revert', 'veth99') + + output = resolvectl('dns', 'veth99') + print(output) + if ipv4: + self.assertIn('8.8.8.8#dns.google', output) + self.assertIn('0.7.4.2#homer.simpson', output) + else: + self.assertNotIn('8.8.8.8#dns.google', output) + if ipv6: + self.assertIn('2001:4860:4860::8888#dns.google', output) + else: + self.assertNotIn('2001:4860:4860::8888#dns.google', output) + + check_json(networkctl_json()) + + copy_network_unit('25-veth.netdev', '25-dhcp-server-veth-peer.network', '25-dhcp-client.network', copy_dropins=False) + + start_networkd() + self.wait_online('veth-peer:carrier') + dnr_v4 = dnr_v4_instance_data(adn = "dns.google", addrs = ["8.8.8.8", "8.8.4.4"]) + dnr_v4 += dnr_v4_instance_data(adn = "homer.simpson", addrs = ["0.7.4.2"], alpns = ("dot","h2","h3"), dohpath = "/springfield{?dns}") + dnr_v6 = dnr_v6_instance_data(adn = "dns.google", addrs = ["2001:4860:4860::8888", "2001:4860:4860::8844"]) + masq = lambda bs: ':'.join(f"{b:02x}" for b in bs) + start_dnsmasq(f'--dhcp-option=162,{masq(dnr_v4)}', + f'--dhcp-option=option6:144,{masq(dnr_v6)}') + + check(self, True, True) + check(self, True, False) + check(self, False, True) + check(self, False, False) + def test_dhcp_client_use_captive_portal(self): def check(self, ipv4, ipv6): os.makedirs(os.path.join(network_unit_dir, '25-dhcp-client.network.d'), exist_ok=True) |