From: Yu Watanabe Date: Sun, 19 Apr 2026 06:42:18 +0000 (+0900) Subject: dhcp-message: introduce dhcp_message_{append,get}_option_hostname() and related functions X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=28423ea18ceeae387ec4a67229f525b62bddcc3d;p=thirdparty%2Fsystemd.git dhcp-message: introduce dhcp_message_{append,get}_option_hostname() and related functions These are for DHCP options 12 (Hostname) and 81 (FQDN) options. --- diff --git a/src/libsystemd-network/dhcp-message.c b/src/libsystemd-network/dhcp-message.c index a2d0cb8c677..92f7d5ed8c0 100644 --- a/src/libsystemd-network/dhcp-message.c +++ b/src/libsystemd-network/dhcp-message.c @@ -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, diff --git a/src/libsystemd-network/dhcp-message.h b/src/libsystemd-network/dhcp-message.h index 4ceba1eec13..ad168838928 100644 --- a/src/libsystemd-network/dhcp-message.h +++ b/src/libsystemd-network/dhcp-message.h @@ -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, diff --git a/src/libsystemd-network/test-dhcp-message.c b/src/libsystemd-network/test-dhcp-message.c index bdb3ef921a2..816edc9fc69 100644 --- a/src/libsystemd-network/test-dhcp-message.c +++ b/src/libsystemd-network/test-dhcp-message.c @@ -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 = {};