]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
dhcp-message: introduce dhcp_message_{append,get}_option_hostname() and related functions 42047/head
authorYu Watanabe <watanabe.yu+github@gmail.com>
Sun, 19 Apr 2026 06:42:18 +0000 (15:42 +0900)
committerYu Watanabe <watanabe.yu+github@gmail.com>
Tue, 12 May 2026 15:27:49 +0000 (00:27 +0900)
These are for DHCP options 12 (Hostname) and 81 (FQDN) options.

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

index a2d0cb8c67744971c540d482d19d9701819a3472..92f7d5ed8c0e5a2e93abf24d2e2f1436fbc43f8a 100644 (file)
@@ -6,7 +6,10 @@
 #include "dhcp-client-id-internal.h"
 #include "dhcp-message.h"
 #include "dhcp-protocol.h"
+#include "dns-domain.h"
+#include "errno-util.h"
 #include "ether-addr-util.h"
+#include "hostname-util.h"
 #include "iovec-util.h"
 #include "iovec-wrapper.h"
 #include "ip-util.h"
@@ -247,6 +250,62 @@ int dhcp_message_append_option_parameter_request_list(sd_dhcp_message *message,
         return dhcp_message_append_option(message, SD_DHCP_OPTION_PARAMETER_REQUEST_LIST, len, buf);
 }
 
+static int dhcp_message_append_option_fqdn(sd_dhcp_message *message, uint8_t flags, bool is_client, const char *fqdn) {
+        int r;
+
+        assert(message);
+        assert(fqdn);
+
+        /* FIXME: Allow long fqdn, as now we support long option. */
+        uint8_t buf[3 + DHCP_MAX_FQDN_LENGTH];
+
+        /* RFC 4702 section 2.1
+         * The "E" bit indicates the encoding of the Domain Name field. 1 indicates canonical wire format,
+         * without compression. This encoding SHOULD be used by clients and MUST be supported by servers.
+         * A server MUST use the same encoding as that used by the client. A server that does not support
+         * the deprecated ASCII encoding MUST ignore Client FQDN options that use that encoding.
+         *
+         * Here, we unconditionally set the 'E' flag. Hence, sd_dhcp_server must ignore the option if a
+         * client does not set the 'E' flag in the request. */
+        buf[0] = flags | DHCP_FQDN_FLAG_E;
+
+        /* RFC 4702 section 2.2
+         * The two 1-octet RCODE1 and RCODE2 fields are deprecated. A client SHOULD set these to 0 when
+         * sending the option and SHOULD ignore them on receipt. A server SHOULD set these to 255 when
+         * sending the option and MUST ignore them on receipt. */
+        buf[1] = is_client ? 0 : 255;
+        buf[2] = is_client ? 0 : 255;
+
+        r = dns_name_to_wire_format(fqdn, buf + 3, sizeof(buf) - 3, false);
+        if (r <= 0)
+                return r;
+
+        return dhcp_message_append_option(message, SD_DHCP_OPTION_FQDN, 3 + r, buf);
+}
+
+int dhcp_message_append_option_hostname(sd_dhcp_message *message, uint8_t flags, bool is_client, const char *hostname) {
+        assert(message);
+
+        /* Hostname (12) or FQDN (81)
+         *
+         * RFC 4702 section 3.1
+         * clients that send the Client FQDN option in their messages MUST NOT also send the Host Name option. */
+
+        if (isempty(hostname))
+                return 0;
+
+        if (dhcp_message_has_option(message, SD_DHCP_OPTION_HOST_NAME))
+                return -EEXIST;
+
+        if (dhcp_message_has_option(message, SD_DHCP_OPTION_FQDN))
+                return -EEXIST;
+
+        if (dns_name_is_single_label(hostname))
+                return dhcp_message_append_option_string(message, SD_DHCP_OPTION_HOST_NAME, hostname);
+
+        return dhcp_message_append_option_fqdn(message, flags, is_client, hostname);
+}
+
 int dhcp_message_get_option(sd_dhcp_message *message, uint8_t code, size_t length, void *ret) {
         int r;
 
@@ -415,6 +474,111 @@ int dhcp_message_get_option_parameter_request_list(sd_dhcp_message *message, Set
         return 0;
 }
 
+static int normalize_dns_name(const char *name, char **ret) {
+        int r;
+
+        assert(name);
+
+        _cleanup_free_ char *normalized = NULL;
+        r = dns_name_normalize(name, /* flags= */ 0, &normalized);
+        if (r < 0)
+                return r;
+
+        if (is_localhost(normalized))
+                return -EINVAL;
+
+        if (dns_name_is_root(normalized))
+                return -EINVAL;
+
+        if (ret)
+                *ret = TAKE_PTR(normalized);
+        return 0;
+}
+
+int dhcp_message_get_option_fqdn(sd_dhcp_message *message, uint8_t *ret_flags, char **ret_fqdn) {
+        int r;
+
+        assert(message);
+
+        _cleanup_(iovec_done) struct iovec iov = {};
+        r = dhcp_message_get_option_alloc(message, SD_DHCP_OPTION_FQDN, &iov);
+        if (r < 0)
+                return r;
+
+        if (iov.iov_len <= 3)
+                return -EBADMSG;
+
+        uint8_t flags = *(uint8_t*) iov.iov_base;
+        if (!FLAGS_SET(flags, DHCP_FQDN_FLAG_E))
+                return -EOPNOTSUPP;
+
+        struct iovec i;
+        iovec_shift(&iov, 3, &i);
+
+        _cleanup_free_ char *name = NULL;
+        const uint8_t *p = i.iov_base;
+        r = dns_name_from_wire_format(&p, &i.iov_len, &name);
+        if (r < 0)
+                return r;
+        if (i.iov_len > 0) /* trailing garbage? */
+                return -EBADMSG;
+
+        if (isempty(name))
+                return -ENODATA;
+
+        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;
+
+        if (ret_flags)
+                *ret_flags = flags;
+        if (ret_fqdn)
+                *ret_fqdn = TAKE_PTR(normalized);
+        return 0;
+}
+
+int dhcp_message_get_option_dns_name(sd_dhcp_message *message, uint8_t code, char **ret) {
+        int r;
+
+        assert(message);
+
+        /* Mainly for Host Name or Domain Name options. */
+
+        _cleanup_free_ char *name = NULL;
+        r = dhcp_message_get_option_string(message, code, &name);
+        if (r < 0)
+                return r;
+
+        _cleanup_free_ char *normalized = NULL;
+        r = normalize_dns_name(name, &normalized);
+        if (r < 0)
+                return r;
+
+        if (ret)
+                *ret = TAKE_PTR(normalized);
+        return 0;
+}
+
+int dhcp_message_get_option_hostname(sd_dhcp_message *message, char **ret) {
+        int r;
+
+        assert(message);
+
+        /* FQDN option always takes precedence. */
+        r = dhcp_message_get_option_fqdn(message, /* ret_flags= */ NULL, ret);
+        if (ERRNO_IS_NEG_RESOURCE(r))
+                return r;
+        if (r >= 0)
+                return 0;
+
+        /* Then, fall back to Host Name option. */
+        return dhcp_message_get_option_dns_name(message, SD_DHCP_OPTION_HOST_NAME, ret);
+}
+
 static int dhcp_message_verify_header(
                 const struct iovec *iov,
                 uint8_t op,
index 4ceba1eec13168220b2ee1d52d8ceff5cd593f4c..ad1688389288eca76c51df57a512ecb7781b61fe 100644 (file)
@@ -41,6 +41,7 @@ int dhcp_message_append_option_addresses(sd_dhcp_message *message, uint8_t code,
 int dhcp_message_append_option_string(sd_dhcp_message *message, uint8_t code, const char *data);
 int dhcp_message_append_option_client_id(sd_dhcp_message *message, const sd_dhcp_client_id *id);
 int dhcp_message_append_option_parameter_request_list(sd_dhcp_message *message, Set *prl);
+int dhcp_message_append_option_hostname(sd_dhcp_message *message, uint8_t flags, bool is_client, const char *hostname);
 
 int dhcp_message_get_option(sd_dhcp_message *message, uint8_t code, size_t length, void *ret);
 int dhcp_message_get_option_alloc(sd_dhcp_message *message, uint8_t code, struct iovec *ret);
@@ -54,6 +55,9 @@ int dhcp_message_get_option_addresses(sd_dhcp_message *message, uint8_t code, si
 int dhcp_message_get_option_string(sd_dhcp_message *message, uint8_t code, char **ret);
 int dhcp_message_get_option_client_id(sd_dhcp_message *message, sd_dhcp_client_id *ret);
 int dhcp_message_get_option_parameter_request_list(sd_dhcp_message *message, Set **ret);
+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_parse(
                 const struct iovec *iov,
index bdb3ef921a2bbf7337337425c14cc8315012de47..816edc9fc69e7edd539dabba7bd99d8d166eeb9a 100644 (file)
@@ -108,6 +108,12 @@ static void verify_prl(sd_dhcp_message *m, Set *expected) {
         ASSERT_TRUE(set_equal(set, expected));
 }
 
+static void verify_hostname(sd_dhcp_message *m, const char *expected) {
+        _cleanup_free_ char *s = NULL;
+        ASSERT_OK(dhcp_message_get_option_hostname(m, &s));
+        ASSERT_STREQ(s, expected);
+}
+
 TEST(dhcp_message) {
         _cleanup_(sd_dhcp_message_unrefp) sd_dhcp_message *m = NULL;
 
@@ -143,6 +149,7 @@ TEST(dhcp_message) {
         for (uint8_t i = SD_DHCP_OPTION_PRIVATE_BASE; i <= SD_DHCP_OPTION_PRIVATE_LAST; i++)
                 ASSERT_OK(set_ensure_put(&prl, /* hash_ops= */ NULL, UINT_TO_PTR(i)));
 
+        const char *hostname = "test-node.example.com";
         const char *vendor_class = "hogehoge";
         char **root_path = STRV_MAKE("/path/to/root", "/hogehoge/foofoo");
 
@@ -222,6 +229,20 @@ TEST(dhcp_message) {
         ASSERT_OK(dhcp_message_append_option_parameter_request_list(m, prl));
         verify_prl(m, prl);
 
+        /* hostname */
+        ASSERT_OK(dhcp_message_append_option_hostname(m, /* flags= */ 0, /* is_client= */ false, "hogehoge"));
+        ASSERT_ERROR(dhcp_message_append_option_hostname(m, /* flags= */ 0, /* is_client= */ false, hostname), EEXIST);
+        dhcp_message_remove_option(m, SD_DHCP_OPTION_FQDN);
+        ASSERT_ERROR(dhcp_message_append_option_hostname(m, /* flags= */ 0, /* is_client= */ false, hostname), EEXIST);
+        dhcp_message_remove_option(m, SD_DHCP_OPTION_HOST_NAME);
+        ASSERT_OK(dhcp_message_append_option_hostname(m, /* flags= */ 0, /* is_client= */ false, "hogehoge.example.com"));
+        ASSERT_ERROR(dhcp_message_append_option_hostname(m, /* flags= */ 0, /* is_client= */ false, hostname), EEXIST);
+        dhcp_message_remove_option(m, SD_DHCP_OPTION_HOST_NAME);
+        ASSERT_ERROR(dhcp_message_append_option_hostname(m, /* flags= */ 0, /* is_client= */ false, hostname), EEXIST);
+        dhcp_message_remove_option(m, SD_DHCP_OPTION_FQDN);
+        ASSERT_OK(dhcp_message_append_option_hostname(m, /* flags= */ 0, /* is_client= */ false, hostname));
+        verify_hostname(m, hostname);
+
         /* build and parse */
         _cleanup_(iovw_done_free) struct iovec_wrapper iovw = {};
         ASSERT_OK(dhcp_message_build(m, &iovw));
@@ -252,6 +273,7 @@ TEST(dhcp_message) {
         verify_string(m2, vendor_class);
         verify_client_id(m2, &id);
         verify_prl(m2, prl);
+        verify_hostname(m2, hostname);
 
         /* build again, and verify the packet */
         _cleanup_(iovw_done_free) struct iovec_wrapper iovw2 = {};