From: Yu Watanabe Date: Sun, 12 Apr 2026 23:54:38 +0000 (+0900) Subject: dhcp-message: introduce dhcp_message_get_option_dnr() X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=7e612d5c15e61497b3aeb64fabe7dd8fe6f4aa40;p=thirdparty%2Fsystemd.git dhcp-message: introduce dhcp_message_get_option_dnr() This is for DHCP option 162 (DNR). --- diff --git a/src/libsystemd-network/dhcp-message.c b/src/libsystemd-network/dhcp-message.c index 676e87d6779..e0ef4f06fb4 100644 --- a/src/libsystemd-network/dhcp-message.c +++ b/src/libsystemd-network/dhcp-message.c @@ -9,6 +9,7 @@ #include "dhcp-route.h" #include "dns-def.h" #include "dns-domain.h" +#include "dns-resolver-internal.h" #include "errno-util.h" #include "ether-addr-util.h" #include "hostname-util.h" @@ -20,6 +21,7 @@ #include "set.h" #include "sort-util.h" #include "string-util.h" +#include "unaligned.h" static sd_dhcp_message* dhcp_message_free(sd_dhcp_message *message) { if (!message) @@ -1164,6 +1166,163 @@ int dhcp_message_get_option_length_prefixed_data( return 0; } +static int parse_dnr_one(const struct iovec *iov, sd_dns_resolver *ret) { + int r; + + assert(iovec_is_set(iov)); + assert(ret); + + _cleanup_(sd_dns_resolver_done) sd_dns_resolver resolver = {}; + struct iovec i = *iov; + + /* service priority */ + if (i.iov_len < sizeof(be16_t)) + return -EBADMSG; + + resolver.priority = unaligned_read_be16(i.iov_base); + iovec_inc(&i, sizeof(be16_t)); + + /* RFC 9460 section 2.4.1: + * When SvcPriority is 0, the SVCB record is in AliasMode. + * + * We do not support the alias mode. But the entry itself is not invalid. */ + if (resolver.priority == 0) { + *ret = (sd_dns_resolver) {}; + return 0; + } + + /* authentication domain name */ + if (!iovec_is_set(&i)) + return -EBADMSG; + + size_t name_len = *(uint8_t*) i.iov_base; + iovec_inc(&i, 1); + if (i.iov_len < name_len) + return -EBADMSG; + + const uint8_t *name_buf = i.iov_base; + iovec_inc(&i, name_len); + + r = dns_name_from_wire_format(&name_buf, &name_len, &resolver.auth_name); + if (r < 0) + return r; + if (r == 0 || name_len != 0) + return -EBADMSG; + + r = dns_name_is_valid_ldh(resolver.auth_name); + if (r < 0) + return r; + if (r == 0) + return -EBADMSG; + + if (dns_name_is_root(resolver.auth_name)) + return -EBADMSG; + + /* RFC9463 section 3.1.6: In ADN-only mode, server omits everything after the ADN. + * + * We don't support these, but they are not invalid. */ + if (!iovec_is_set(&i)) { + *ret = (sd_dns_resolver) {}; + return 0; + } + + /* IPv4 addresses */ + size_t n = *(uint8_t*) i.iov_base; + iovec_inc(&i, 1); + + if (n % sizeof(struct in_addr) != 0) + return -EBADMSG; + + n /= sizeof(struct in_addr); + + /* RFC9463 section 3.1.8: option MUST include at least one valid IP addr */ + if (n == 0) + return -EBADMSG; + + resolver.family = AF_INET; + resolver.n_addrs = n; + resolver.addrs = new(union in_addr_union, n); + if (!resolver.addrs) + return -ENOMEM; + + for (size_t j = 0; j < n; j++) { + if (i.iov_len < sizeof(struct in_addr)) + return -EBADMSG; + + struct in_addr a; + memcpy(&a, i.iov_base, sizeof(struct in_addr)); + iovec_inc(&i, sizeof(struct in_addr)); + + /* RFC9463 section 5.2: client MUST discard multicast and host loopback addresses */ + if (in4_addr_is_multicast(&a) || in4_addr_is_localhost(&a)) + return -EBADMSG; + + resolver.addrs[j] = (union in_addr_union) { .in = a }; + } + + /* service params */ + r = dnr_parse_svc_params(i.iov_base, i.iov_len, &resolver); + if (r < 0) + return r; + if (r == 0) { + /* We can't use this record, but it is not invalid. */ + *ret = (sd_dns_resolver) {}; + return 0; + } + + *ret = TAKE_STRUCT(resolver); + return 1; +} + +int dhcp_message_get_option_dnr(sd_dhcp_message *message, size_t *ret_n_resolvers, sd_dns_resolver **ret_resolvers) { + int r; + + assert(message); + assert(ret_n_resolvers || !ret_resolvers); + + /* See RFC 9463 section 5.1 */ + + _cleanup_(iovw_done_free) struct iovec_wrapper iovw = {}; + r = dhcp_message_get_option_length_prefixed_data(message, SD_DHCP_OPTION_V4_DNR, /* length_size= */ 2, &iovw); + if (r < 0) + return r; + + sd_dns_resolver *resolvers = NULL; + size_t n_resolvers = 0; + CLEANUP_ARRAY(resolvers, n_resolvers, dns_resolver_free_array); + FOREACH_ARRAY(i, iovw.iovec, iovw.count) { + _cleanup_(sd_dns_resolver_done) sd_dns_resolver dnr = {}; + r = parse_dnr_one(i, &dnr); + if (r < 0) + return r; + if (r == 0) + continue; + + if (!ret_resolvers) { + n_resolvers++; + continue; + } + + if (!GREEDY_REALLOC(resolvers, n_resolvers + 1)) + return -ENOMEM; + + resolvers[n_resolvers++] = TAKE_STRUCT(dnr); + } + + if (n_resolvers == 0) /* no supported resolver */ + return -ENODATA; + + if (ret_resolvers) { + /* Sort the resolvers with their priorities. */ + typesafe_qsort(resolvers, n_resolvers, dns_resolver_prio_compare); + *ret_resolvers = TAKE_PTR(resolvers); + } + if (ret_n_resolvers) + *ret_n_resolvers = n_resolvers; + + return 0; +} + static int dhcp_message_verify_header( const struct iovec *iov, uint8_t op, diff --git a/src/libsystemd-network/dhcp-message.h b/src/libsystemd-network/dhcp-message.h index f754bde05ad..e003d9cc5bf 100644 --- a/src/libsystemd-network/dhcp-message.h +++ b/src/libsystemd-network/dhcp-message.h @@ -79,6 +79,7 @@ int dhcp_message_get_option_hostname(sd_dhcp_message *message, char **ret); int dhcp_message_get_option_domains(sd_dhcp_message *message, uint8_t code, char ***ret); int dhcp_message_get_option_sub_tlv(sd_dhcp_message *message, uint8_t code, TLVFlag flags, TLV **ret); int dhcp_message_get_option_length_prefixed_data(sd_dhcp_message *message, uint8_t code, size_t length_size, struct iovec_wrapper *ret); +int dhcp_message_get_option_dnr(sd_dhcp_message *message, size_t *ret_n_resolvers, sd_dns_resolver **ret_resolvers); int dhcp_message_parse( const struct iovec *iov, diff --git a/src/libsystemd-network/test-dhcp-message.c b/src/libsystemd-network/test-dhcp-message.c index dd4f49f1fb1..7f5d6669a94 100644 --- a/src/libsystemd-network/test-dhcp-message.c +++ b/src/libsystemd-network/test-dhcp-message.c @@ -7,6 +7,8 @@ #include "dhcp-message.h" #include "dhcp-protocol.h" #include "dhcp-route.h" +#include "dns-packet.h" +#include "dns-resolver-internal.h" #include "ether-addr-util.h" #include "iovec-util.h" #include "iovec-wrapper.h" @@ -575,4 +577,196 @@ TEST(domains) { test_domains_fail(ELEMENTSOF(truncated), truncated); } +TEST(dnr) { + _cleanup_(sd_dhcp_message_unrefp) sd_dhcp_message *m = NULL; + ASSERT_OK(dhcp_message_new(&m)); + + sd_dns_resolver *resolvers = NULL; + size_t n_resolvers = 0; + CLEANUP_ARRAY(resolvers, n_resolvers, dns_resolver_free_array); + + static uint8_t data[] = { + /* Instance 1 */ + /* length */ + 0, 78, + /* priority */ + 0, 1, + /* authentication domain name */ + 22, + 8, 'r', 'e', 's', 'o', 'l', 'v', 'e', 'r', + 7, 'e', 'x', 'a', 'm', 'p', 'l', 'e', + 3, 'c', 'o', 'm', + 0, + /* addresses */ + 8, + 192, 0, 2, 1, + 192, 0, 2, 2, + /* service parameters */ + /* ALPN */ + 0, DNS_SVC_PARAM_KEY_ALPN, + 0, 14, + 2, 'h', '2', + 2, 'h', '3', + 3, 'd', 'o', 't', + 3, 'd', 'o', 'q', + /* port */ + 0, DNS_SVC_PARAM_KEY_PORT, + 0, 2, + 0, 42, + /* DoH path*/ + 0, DNS_SVC_PARAM_KEY_DOHPATH, + 0, 16, + '/', 'd', 'n', 's', '-', 'q', 'u', 'e', 'r', 'y', '{', '?', 'd', 'n', 's', '}', + + /* Instance 2 */ + /* length */ + 0, 44, + /* priority */ + 0, 2, + /* authentication domain name */ + 22, + 8, 'h', 'o', 'g', 'e', 'h', 'o', 'g', 'e', + 7, 'e', 'x', 'a', 'm', 'p', 'l', 'e', + 3, 'c', 'o', 'm', + 0, + /* addresses */ + 4, + 192, 0, 2, 3, + /* service parameters */ + /* ALPN */ + 0, DNS_SVC_PARAM_KEY_ALPN, + 0, 4, + 3, 'd', 'o', 't', + /* port */ + 0, DNS_SVC_PARAM_KEY_PORT, + 0, 2, + 0, 33, + + /* Instance 3 (no address, ignored) */ + /* length */ + 0, 20, + /* priority */ + 0, 3, + /* authentication domain name */ + 17, + 3, 'f', 'o', 'o', + 7, 'e', 'x', 'a', 'm', 'p', 'l', 'e', + 3, 'c', 'o', 'm', + 0, + + /* Instance 4 (unknown alpn, ignored) */ + /* length */ + 0, 37, + /* priority */ + 0, 4, + /* authentication domain name */ + 20, + 6, 'b', 'a', 'r', 'b', 'a', 'z', + 7, 'e', 'x', 'a', 'm', 'p', 'l', 'e', + 3, 'c', 'o', 'm', + 0, + /* addresses */ + 4, + 192, 0, 2, 4, + /* service parameters */ + /* ALPN */ + 0, DNS_SVC_PARAM_KEY_ALPN, + 0, 5, + 4, 'h', 'o', 'g', 'e', + }; + + ASSERT_OK(dhcp_message_append_option(m, SD_DHCP_OPTION_V4_DNR, ELEMENTSOF(data), data)); + ASSERT_OK(dhcp_message_get_option_dnr(m, &n_resolvers, &resolvers)); + ASSERT_EQ(n_resolvers, 2u); + + ASSERT_EQ(resolvers[0].priority, 1u); + ASSERT_STREQ(resolvers[0].auth_name, "resolver.example.com"); + ASSERT_EQ(resolvers[0].family, AF_INET); + ASSERT_EQ(resolvers[0].n_addrs, 2u); + ASSERT_STREQ(IN_ADDR_TO_STRING(resolvers[0].family, &resolvers[0].addrs[0]), "192.0.2.1"); + ASSERT_STREQ(IN_ADDR_TO_STRING(resolvers[0].family, &resolvers[0].addrs[1]), "192.0.2.2"); + ASSERT_EQ(resolvers[0].transports, SD_DNS_ALPN_HTTP_2_TLS | SD_DNS_ALPN_HTTP_3 | SD_DNS_ALPN_DOT | SD_DNS_ALPN_DOQ); + ASSERT_EQ(resolvers[0].port, 42u); + ASSERT_STREQ(resolvers[0].dohpath, "/dns-query{?dns}"); + + ASSERT_EQ(resolvers[1].priority, 2u); + ASSERT_STREQ(resolvers[1].auth_name, "hogehoge.example.com"); + ASSERT_EQ(resolvers[1].family, AF_INET); + ASSERT_EQ(resolvers[1].n_addrs, 1u); + ASSERT_STREQ(IN_ADDR_TO_STRING(resolvers[1].family, &resolvers[1].addrs[0]), "192.0.2.3"); + ASSERT_EQ(resolvers[1].transports, SD_DNS_ALPN_DOT); + ASSERT_EQ(resolvers[1].port, 33u); + ASSERT_NULL(resolvers[1].dohpath); + + /* missing DoH path */ + static uint8_t invalid[] = { + /* length */ + 0, 35, + /* priority */ + 0, 5, + /* authentication domain name */ + 20, + 6, 'b', 'a', 'r', 'b', 'a', 'z', + 7, 'e', 'x', 'a', 'm', 'p', 'l', 'e', + 3, 'c', 'o', 'm', + 0, + /* addresses */ + 4, + 192, 0, 2, 5, + /* service parameters */ + /* ALPN */ + 0, DNS_SVC_PARAM_KEY_ALPN, + 0, 3, + 2, 'h', '2', + }; + ASSERT_OK(dhcp_message_append_option(m, SD_DHCP_OPTION_V4_DNR, ELEMENTSOF(invalid), invalid)); + ASSERT_ERROR(dhcp_message_get_option_dnr(m, NULL, NULL), EBADMSG); + + dhcp_message_remove_option(m, SD_DHCP_OPTION_V4_DNR); + + /* missing ALPN */ + static uint8_t invalid2[] = { + /* length */ + 0, 28, + /* priority */ + 0, 6, + /* authentication domain name */ + 20, + 6, 'b', 'a', 'r', 'b', 'a', 'z', + 7, 'e', 'x', 'a', 'm', 'p', 'l', 'e', + 3, 'c', 'o', 'm', + 0, + /* addresses */ + 4, + 192, 0, 2, 6, + }; + ASSERT_OK(dhcp_message_append_option(m, SD_DHCP_OPTION_V4_DNR, ELEMENTSOF(invalid2), invalid2)); + ASSERT_ERROR(dhcp_message_get_option_dnr(m, NULL, NULL), EBADMSG); + + dhcp_message_remove_option(m, SD_DHCP_OPTION_V4_DNR); + + /* truncated domain name */ + static uint8_t invalid3[] = { + /* length */ + 0, 34, + /* priority */ + 0, 7, + /* authentication domain name */ + 18, + 6, 'b', 'a', 'r', 'b', 'a', 'z', + 7, 'e', 'x', 'a', 'm', 'p', 'l', 'e', + 3, 'c', 'o', + /* addresses */ + 4, + 192, 0, 2, 7, + /* service parameters */ + /* ALPN */ + 0, DNS_SVC_PARAM_KEY_ALPN, + 0, 4, + 3, 'd', 'o', 't', + }; + ASSERT_OK(dhcp_message_append_option(m, SD_DHCP_OPTION_V4_DNR, ELEMENTSOF(invalid3), invalid3)); + ASSERT_ERROR(dhcp_message_get_option_dnr(m, NULL, NULL), EMSGSIZE); +} + DEFINE_TEST_MAIN(LOG_DEBUG);