]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
dhcp-message: introduce dhcp_message_get_option_domains()
authorYu Watanabe <watanabe.yu+github@gmail.com>
Sat, 11 Apr 2026 17:59:16 +0000 (02:59 +0900)
committerYu Watanabe <watanabe.yu+github@gmail.com>
Wed, 13 May 2026 00:41:24 +0000 (09:41 +0900)
This is for e.g. DHCP option 119 (domain search).

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

index c2c25ed6ff665f73c053dd65e1c0be674451d528..00e38ea9eeca5a627db88535031b650ef51ca342 100644 (file)
@@ -6,6 +6,7 @@
 #include "dhcp-client-id-internal.h"
 #include "dhcp-message.h"
 #include "dhcp-protocol.h"
+#include "dns-def.h"
 #include "dns-domain.h"
 #include "errno-util.h"
 #include "ether-addr-util.h"
@@ -619,6 +620,108 @@ int dhcp_message_get_option_hostname(sd_dhcp_message *message, char **ret) {
         return dhcp_message_get_option_dns_name(message, SD_DHCP_OPTION_HOST_NAME, ret);
 }
 
+int dhcp_message_get_option_domains(sd_dhcp_message *message, uint8_t code, char ***ret) {
+        int r;
+
+        assert(message);
+
+        /* This is mostly for SD_DHCP_OPTION_DOMAIN_SEARCH. */
+
+        _cleanup_(iovec_done) struct iovec iov = {};
+        r = dhcp_message_get_option_alloc(message, code, &iov);
+        if (r < 0)
+                return r;
+
+        const uint8_t *buf = iov.iov_base;
+        size_t len = iov.iov_len;
+
+        _cleanup_strv_free_ char **names = NULL;
+        size_t n_names = 0;
+
+        _cleanup_free_ char *name = NULL;
+        size_t n = 0;
+
+        for (size_t pos = 0, jump_barrier = 0, next_chunk = 0; pos < len;) {
+                uint8_t c = buf[pos++];
+
+                if (c == 0) {
+                        /* End of name */
+
+                        if (!string_is_safe(name, /* flags= */ 0))
+                                return -EBADMSG;
+
+                        _cleanup_free_ char *normalized = NULL;
+                        r = normalize_dns_name(name, &normalized);
+                        if (r < 0)
+                                return r;
+
+                        r = strv_consume_with_size(&names, &n_names, TAKE_PTR(normalized));
+                        if (r < 0)
+                                return r;
+
+                        if (next_chunk != 0)
+                                pos = next_chunk;
+
+                        next_chunk = 0;
+                        jump_barrier = pos;
+
+                        name = mfree(name);
+                        n = 0;
+
+                } else if (c <= 63) {
+                        /* Literal label */
+
+                        const char *label = (const char*) (buf + pos);
+                        pos += c;
+
+                        if (pos >= len)
+                                return -EBADMSG;
+
+                        if (!GREEDY_REALLOC(name, n + 1 + DNS_LABEL_ESCAPED_MAX))
+                                return -ENOMEM;
+
+                        if (n != 0)
+                                name[n++] = '.';
+
+                        r = dns_label_escape(label, c, name + n, DNS_LABEL_ESCAPED_MAX);
+                        if (r < 0)
+                                return r;
+
+                        n += r;
+
+                } else if (FLAGS_SET(c, 0xc0)) {
+                        /* Pointer */
+
+                        if (pos >= len) /* pointer is 2 bytes, hence we need to read at least one more byte. */
+                                return -EBADMSG;
+
+                        /* Save the current location so we don't end up re-parsing what's parsed so far. */
+                        if (next_chunk == 0)
+                                next_chunk = pos + 1;
+
+                        pos = ((size_t) (c & ~0xc0) << 8) | ((size_t) buf[pos]);
+
+                        /* Jumps are limited to a "prior occurrence" (RFC-1035 4.1.4) */
+                        if (pos >= jump_barrier)
+                                return -EBADMSG;
+
+                        jump_barrier = pos;
+
+                } else
+                        return -EBADMSG;
+        }
+
+        if (!isempty(name)) /* trailing garbage?? Should not happen, but for safety. */
+                return -EBADMSG;
+
+        if (strv_isempty(names))
+                return -EBADMSG;
+
+        if (ret)
+                *ret = TAKE_PTR(names);
+        return 0;
+}
+
 int dhcp_message_get_option_sub_tlv(sd_dhcp_message *message, uint8_t code, TLVFlag flags, TLV **ret) {
         int r;
 
index 4b85a26e9221326d3bcb08931e548a9b8331507c..e9e4551cd80bca9cb930c9b73062c6267ff892bf 100644 (file)
@@ -60,6 +60,7 @@ int dhcp_message_get_option_parameter_request_list(sd_dhcp_message *message, Set
 int dhcp_message_get_option_fqdn(sd_dhcp_message *message, uint8_t *ret_flags, char **ret_fqdn);
 int dhcp_message_get_option_dns_name(sd_dhcp_message *message, uint8_t code, char **ret);
 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);
 
index effdf92dc709bd3721ea508f177abe7b94d1b7a4..ddb3823f25435a6ea8c8c2e24487c4fd9aa0b6b3 100644 (file)
@@ -330,4 +330,122 @@ TEST(dhcp_message) {
         ASSERT_TRUE(iovw_equal(&iovw, &iovw2));
 }
 
+static void test_domains_one(size_t len, const uint8_t *data, char * const *expected) {
+        _cleanup_strv_free_ char **strv = NULL;
+        _cleanup_(sd_dhcp_message_unrefp) sd_dhcp_message *m = NULL;
+        ASSERT_OK(dhcp_message_new(&m));
+
+        ASSERT_OK(dhcp_message_append_option(m, SD_DHCP_OPTION_DOMAIN_SEARCH, len, data));
+        ASSERT_OK(dhcp_message_get_option_domains(m, SD_DHCP_OPTION_DOMAIN_SEARCH, &strv));
+        ASSERT_TRUE(strv_equal(strv, expected));
+
+        dhcp_message_remove_option(m, SD_DHCP_OPTION_DOMAIN_SEARCH);
+        strv = strv_free(strv);
+
+        ASSERT_OK(dhcp_message_append_option(m, SD_DHCP_OPTION_DOMAIN_SEARCH, len / 2, data));
+        ASSERT_OK(dhcp_message_append_option(m, SD_DHCP_OPTION_DOMAIN_SEARCH, len - len / 2, data + len / 2));
+        ASSERT_OK(dhcp_message_get_option_domains(m, SD_DHCP_OPTION_DOMAIN_SEARCH, &strv));
+        ASSERT_TRUE(strv_equal(strv, expected));
+}
+
+static void test_domains_fail(size_t len, const uint8_t *data) {
+        _cleanup_(sd_dhcp_message_unrefp) sd_dhcp_message *m = NULL;
+        ASSERT_OK(dhcp_message_new(&m));
+
+        ASSERT_OK(dhcp_message_append_option(m, SD_DHCP_OPTION_DOMAIN_SEARCH, len, data));
+        ASSERT_ERROR(dhcp_message_get_option_domains(m, SD_DHCP_OPTION_DOMAIN_SEARCH, /* ret= */ NULL), EBADMSG);
+
+        dhcp_message_remove_option(m, SD_DHCP_OPTION_DOMAIN_SEARCH);
+
+        _cleanup_free_ uint8_t *sip = new(uint8_t, len + 1);
+        sip[0] = 0;
+        memcpy(sip + 1, data, len);
+
+        ASSERT_OK(dhcp_message_append_option(m, SD_DHCP_OPTION_SIP_SERVER, len + 1, sip));
+        ASSERT_ERROR(dhcp_message_get_option_domains(m, SD_DHCP_OPTION_SIP_SERVER, /* ret= */ NULL), EBADMSG);
+}
+
+TEST(domains) {
+        /* simple */
+        static uint8_t simple[] = {
+                7, 'e', 'x', 'a', 'm', 'p', 'l', 'e',
+                3, 'c', 'o', 'm',
+                0,
+                4, 'h', 'o', 'g', 'e',
+                3, 'f', 'o', 'o',
+                0,
+        };
+        test_domains_one(ELEMENTSOF(simple), simple,
+                         STRV_MAKE("example.com", "hoge.foo"));
+
+        /* compressed */
+        static uint8_t compressed[] = {
+                4, 'h', 'o', 'g', 'e',
+                7, 'e', 'x', 'a', 'm', 'p', 'l', 'e',
+                3, 'c', 'o', 'm',
+                0,
+                3, 'f', 'o', 'o',
+                0xc0, 5,
+                3, 'b', 'a', 'r',
+                0xc0, 18,
+                3, 'b', 'a', 'z',
+                0xc0, 0,
+        };
+        test_domains_one(ELEMENTSOF(compressed), compressed,
+                         STRV_MAKE("hoge.example.com", "foo.example.com", "bar.foo.example.com", "baz.hoge.example.com"));
+
+        /* invalid pointer */
+        static uint8_t invalid[] = {
+                7, 'e', 'x', 'a', 'm', 'p', 'l', 'e',
+                3, 'c', 'o', 'm',
+                0,
+                3, 'f', 'o', 'o',
+                0xc0, 0xff,
+        };
+        test_domains_fail(ELEMENTSOF(invalid), invalid);
+
+        /* forward pointer */
+        static uint8_t forward[] = {
+                4, 'h', 'o', 'g', 'e',
+                0xc0, 11,
+                3, 'f', 'o', 'o',
+                7, 'e', 'x', 'a', 'm', 'p', 'l', 'e',
+                3, 'c', 'o', 'm',
+                0,
+        };
+        test_domains_fail(ELEMENTSOF(forward), forward);
+
+        /* infinite loop */
+        static uint8_t loop1[] = {
+                0xc0, 0x00,
+        };
+        test_domains_fail(ELEMENTSOF(loop1), loop1);
+
+        static uint8_t loop2[] = {
+                0xc0, 0x02,
+                0xc0, 0x00,
+        };
+        test_domains_fail(ELEMENTSOF(loop2), loop2);
+
+        static uint8_t loop3[] = {
+                7, 'e', 'x', 'a', 'm', 'p', 'l', 'e',
+                0xc0, 0x00,
+        };
+        test_domains_fail(ELEMENTSOF(loop3), loop3);
+
+        /* unterminated */
+        static uint8_t unterminated[] = {
+                7, 'e', 'x', 'a', 'm', 'p', 'l', 'e',
+                3, 'c', 'o', 'm',
+        };
+        test_domains_fail(ELEMENTSOF(unterminated), unterminated);
+
+        /* truncated label */
+        static uint8_t truncated[] = {
+                7, 'e', 'x', 'a', 'm', 'p', 'l', 'e',
+                3, 'c', 'o',
+        };
+        test_domains_fail(ELEMENTSOF(truncated), truncated);
+}
+
 DEFINE_TEST_MAIN(LOG_DEBUG);