]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
dhcp-message: introduce dhcp_message_get_option_dnr() 42063/head
authorYu Watanabe <watanabe.yu+github@gmail.com>
Sun, 12 Apr 2026 23:54:38 +0000 (08:54 +0900)
committerYu Watanabe <watanabe.yu+github@gmail.com>
Wed, 13 May 2026 00:41:24 +0000 (09:41 +0900)
This is for DHCP option 162 (DNR).

src/libsystemd-network/dhcp-message.c
src/libsystemd-network/dhcp-message.h
src/libsystemd-network/test-dhcp-message.c

index 676e87d6779a58a8ca48395e84a95930c5537509..e0ef4f06fb481b4c67644785fd66a628b974182d 100644 (file)
@@ -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,
index f754bde05ad727a69430cde7a620fd83100e39ca..e003d9cc5bf20ea98b40ce8668b8bf0dddf841f7 100644 (file)
@@ -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,
index dd4f49f1fb16e40bf9f5559c52a6bbbed3d88c66..7f5d6669a94956d5a79671b4f06eb4b202ba5b6f 100644 (file)
@@ -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);