From: Chris Down Date: Sun, 9 Nov 2025 16:59:59 +0000 (+0800) Subject: sd-dhcp-server: Add Hostname= option to static leases X-Git-Tag: v259-rc1~62 X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=7f9c0c31d2f00f472c361868ac2184d77113db72;p=thirdparty%2Fsystemd.git sd-dhcp-server: Add Hostname= option to static leases This adds a new `Hostname=` option to the [DHCPServerStaticLease] section in .network files, allowing an administrator to assign a specific hostname to a client receiving a static lease. We automatically select the correct DHCP option to use based on the format of the provided string: - Single DNS labels are sent as Option 12. - Names with multiple DNS labels are sent as Option 81 in wire format. Fixes: #39634 --- diff --git a/man/systemd.network.xml b/man/systemd.network.xml index 4710c7fe7fd..a75f89de99d 100644 --- a/man/systemd.network.xml +++ b/man/systemd.network.xml @@ -4227,6 +4227,25 @@ ServerAddress=192.168.0.1/24 + + + Hostname= + + The hostname to send to the client in DHCP replies. This can be either a simple + hostname (e.g., mydevice) or a fully qualified domain name (e.g., + mydevice.example.com), following RFC 1123 naming conventions. Each label can be + up to 63 characters, with a total maximum length of 253 characters for FQDNs. When this option is + set, the DHCP server will include the hostname in DHCP replies (both OFFER and ACK) to the client + with the matched MAC address. + + The server automatically selects the appropriate DHCP option based on the hostname format: + simple hostnames (single DNS label) are sent via option 12 (Host Name) per RFC 2132, while FQDNs + with multiple labels are sent via option 81 (Client FQDN) per RFC 4702 using DNS wire format + encoding. The configured hostname is sent unconditionally, any hostname requested by the client in + its DHCP message is ignored. + + + diff --git a/src/libsystemd-network/fuzz-dhcp-server.c b/src/libsystemd-network/fuzz-dhcp-server.c index 47e3c9efa97..a91964fe0df 100644 --- a/src/libsystemd-network/fuzz-dhcp-server.c +++ b/src/libsystemd-network/fuzz-dhcp-server.c @@ -56,9 +56,11 @@ static int add_static_lease(sd_dhcp_server *server, uint8_t i) { assert(server); return sd_dhcp_server_set_static_lease( - server, - &(struct in_addr) { .s_addr = htobe32(UINT32_C(10) << 24 | i)}, - id, ELEMENTSOF(id)); + server, + &(struct in_addr) { .s_addr = htobe32(UINT32_C(10) << 24 | i) }, + id, + ELEMENTSOF(id), + /* hostname= */ NULL); } int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { diff --git a/src/libsystemd-network/sd-dhcp-lease.c b/src/libsystemd-network/sd-dhcp-lease.c index a98ec7fc63b..fcf732bec44 100644 --- a/src/libsystemd-network/sd-dhcp-lease.c +++ b/src/libsystemd-network/sd-dhcp-lease.c @@ -505,6 +505,60 @@ static int lease_parse_domain(const uint8_t *option, size_t len, char **ret) { return 0; } +static int lease_parse_fqdn(const uint8_t *option, size_t len, char **hostname) { + _cleanup_free_ char *name = NULL, *normalized = NULL; + int r; + + assert(option); + assert(hostname); + + /* RFC 4702 Section 2 + * + * Byte 0: Flags (S: server should perform A RR updates, O: override existing A RR, + * E: encoding (0=ASCII, 1=Wire format), N: no server updates) + * Byte 1: RCODE1 (ignored on receipt) + * Byte 2: RCODE2 (ignored on receipt) + * Bytes 3+: Domain Name */ + + if (len <= 3) + return -EBADMSG; + + size_t data_len = len - 3; + const uint8_t *data = option + 3; + + /* In practice, many servers send DNS wire format regardless of the E flag, so ignore and try wire + * format first, then fall back to ASCII if that fails. */ + r = dns_name_from_wire_format(&data, &data_len, &name); + if (r < 0) { + if (FLAGS_SET(option[0], DHCP_FQDN_FLAG_E)) + return -EBADMSG; + + /* Wire format failed, try ASCII format */ + r = dhcp_option_parse_string(option + 3, len - 3, &name); + if (r < 0) + return r; + } + + if (!name) { + *hostname = mfree(*hostname); + return 0; + } + + r = dns_name_normalize(name, 0, &normalized); + if (r < 0) + return r; + + if (is_localhost(normalized)) + return -EINVAL; + + if (dns_name_is_root(normalized)) + return -EINVAL; + + free_and_replace(*hostname, normalized); + + return 0; +} + static int lease_parse_captive_portal(const uint8_t *option, size_t len, char **ret) { _cleanup_free_ char *uri = NULL; int r; @@ -967,6 +1021,12 @@ int dhcp_lease_parse_options(uint8_t code, uint8_t len, const void *option, void break; case SD_DHCP_OPTION_HOST_NAME: + /* FQDN option (81) always takes precedence. If it was already set, do not overwrite it. */ + if (lease->hostname) { + log_debug("Hostname already set via FQDN, ignoring hostname option."); + break; + } + r = lease_parse_domain(option, len, &lease->hostname); if (r < 0) { log_debug_errno(r, "Failed to parse hostname, ignoring: %m"); @@ -975,6 +1035,15 @@ int dhcp_lease_parse_options(uint8_t code, uint8_t len, const void *option, void break; + case SD_DHCP_OPTION_FQDN: + r = lease_parse_fqdn(option, len, &lease->hostname); + if (r < 0) { + log_debug_errno(r, "Failed to parse FQDN, ignoring: %m"); + return 0; + } + + break; + case SD_DHCP_OPTION_ROOT_PATH: { _cleanup_free_ char *p = NULL; diff --git a/src/libsystemd-network/sd-dhcp-server-lease.c b/src/libsystemd-network/sd-dhcp-server-lease.c index 1daafde7164..68b3ac5bccd 100644 --- a/src/libsystemd-network/sd-dhcp-server-lease.c +++ b/src/libsystemd-network/sd-dhcp-server-lease.c @@ -7,6 +7,7 @@ #include "alloc-util.h" #include "dhcp-server-lease-internal.h" +#include "dns-domain.h" #include "errno-util.h" #include "fd-util.h" #include "fs-util.h" @@ -181,7 +182,8 @@ int sd_dhcp_server_set_static_lease( sd_dhcp_server *server, const struct in_addr *address, uint8_t *client_id_raw, - size_t client_id_size) { + size_t client_id_size, + const char *hostname) { _cleanup_(sd_dhcp_server_lease_unrefp) sd_dhcp_server_lease *lease = NULL; sd_dhcp_client_id client_id; @@ -203,6 +205,14 @@ int sd_dhcp_server_set_static_lease( return 0; } + if (hostname) { + r = dns_name_is_valid_ldh(hostname); + if (r < 0) + return r; + if (r == 0) + return -EINVAL; + } + lease = new(sd_dhcp_server_lease, 1); if (!lease) return -ENOMEM; @@ -213,6 +223,12 @@ int sd_dhcp_server_set_static_lease( .client_id = client_id, }; + if (hostname) { + lease->hostname = strdup(hostname); + if (!lease->hostname) + return -ENOMEM; + } + r = dhcp_server_put_lease(server, lease, /* is_static = */ true); if (r < 0) return r; diff --git a/src/libsystemd-network/sd-dhcp-server.c b/src/libsystemd-network/sd-dhcp-server.c index 3a4d3aa2028..75031606436 100644 --- a/src/libsystemd-network/sd-dhcp-server.c +++ b/src/libsystemd-network/sd-dhcp-server.c @@ -534,6 +534,64 @@ static int server_message_init( return 0; } +static int dhcp_server_append_static_hostname( + sd_dhcp_server *server, + DHCPPacket *packet, + size_t *offset, + DHCPRequest *req) { + + sd_dhcp_server_lease *static_lease; + int r; + + assert(server); + assert(packet); + assert(offset); + assert(req); + + static_lease = dhcp_server_get_static_lease(server, req); + if (!static_lease || !static_lease->hostname) + return 0; + + if (dns_name_is_single_label(static_lease->hostname)) + /* Option 12 */ + return dhcp_option_append( + &packet->dhcp, + req->max_optlen, + offset, + /* overload= */ 0, + SD_DHCP_OPTION_HOST_NAME, + strlen(static_lease->hostname), + static_lease->hostname); + + + /* Option 81 */ + uint8_t buffer[DHCP_MAX_FQDN_LENGTH + 3]; + + /* Flags: S=0 (will not update RR), O=1 (are overriding client), + * E=1 (using DNS wire format), N=1 (will not update DNS) */ + buffer[0] = DHCP_FQDN_FLAG_O | DHCP_FQDN_FLAG_E | DHCP_FQDN_FLAG_N; + + /* RFC 4702: A server SHOULD set these to 255 when sending the option and MUST ignore them on + * receipt. */ + buffer[1] = 255; + buffer[2] = 255; + + r = dns_name_to_wire_format(static_lease->hostname, buffer + 3, sizeof(buffer) - 3, false); + if (r < 0) + return log_dhcp_server_errno(server, r, "Failed to encode FQDN for static lease: %m"); + if (r > DHCP_MAX_FQDN_LENGTH) + return log_dhcp_server_errno(server, SYNTHETIC_ERRNO(EINVAL), "FQDN for static lease too long"); + + return dhcp_option_append( + &packet->dhcp, + req->max_optlen, + offset, + /* overload= */ 0, + SD_DHCP_OPTION_FQDN, + 3 + r, + buffer); +} + static int server_send_offer_or_ack( sd_dhcp_server *server, DHCPRequest *req, @@ -675,6 +733,10 @@ static int server_send_offer_or_ack( return r; } + r = dhcp_server_append_static_hostname(server, packet, &offset, req); + if (r < 0) + return r; + return dhcp_server_send_packet(server, req, packet, type, offset); } diff --git a/src/libsystemd-network/test-dhcp-server.c b/src/libsystemd-network/test-dhcp-server.c index 705c0e92beb..7789037011e 100644 --- a/src/libsystemd-network/test-dhcp-server.c +++ b/src/libsystemd-network/test-dhcp-server.c @@ -139,8 +139,12 @@ static void test_message_handler(void) { ASSERT_OK(sd_dhcp_server_new(&server, 1)); ASSERT_OK(sd_dhcp_server_configure_pool(server, &address_lo, 8, 0, 0)); - ASSERT_OK(sd_dhcp_server_set_static_lease(server, &static_lease_address, static_lease_client_id, - ELEMENTSOF(static_lease_client_id))); + ASSERT_OK(sd_dhcp_server_set_static_lease( + server, + &static_lease_address, + static_lease_client_id, + ELEMENTSOF(static_lease_client_id), + /* hostname= */ NULL)); ASSERT_OK(sd_dhcp_server_attach_event(server, NULL, 0)); ASSERT_OK(sd_dhcp_server_start(server)); @@ -216,8 +220,12 @@ static void test_message_handler(void) { /* add the static lease for the client ID */ ASSERT_OK(sd_dhcp_server_stop(server)); - ASSERT_OK(sd_dhcp_server_set_static_lease(server, &(struct in_addr){ .s_addr = htobe32(INADDR_LOOPBACK + 31) }, - (uint8_t[7]){ 0x01, 'A', 'B', 'C', 'D', 'E', 'F' }, 7)); + ASSERT_OK(sd_dhcp_server_set_static_lease( + server, + &(struct in_addr) { .s_addr = htobe32(INADDR_LOOPBACK + 31) }, + (uint8_t[7]) { 0x01, 'A', 'B', 'C', 'D', 'E', 'F' }, + 7, + /* hostname= */ NULL)); ASSERT_OK(sd_dhcp_server_start(server)); /* discover */ @@ -244,7 +252,12 @@ static void test_message_handler(void) { /* drop the static lease for the client ID */ ASSERT_OK(sd_dhcp_server_stop(server)); - ASSERT_OK(sd_dhcp_server_set_static_lease(server, NULL, (uint8_t[7]){ 0x01, 'A', 'B', 'C', 'D', 'E', 'F' }, 7)); + ASSERT_OK(sd_dhcp_server_set_static_lease( + server, + /* address= */ NULL, + (uint8_t[7]) { 0x01, 'A', 'B', 'C', 'D', 'E', 'F' }, + 7, + /* hostname= */ NULL)); ASSERT_OK(sd_dhcp_server_start(server)); /* request a new non-static address */ @@ -324,35 +337,78 @@ static void test_static_lease(void) { ASSERT_OK(sd_dhcp_server_new(&server, 1)); - ASSERT_OK(sd_dhcp_server_set_static_lease(server, &(struct in_addr) { .s_addr = 0x01020304 }, - (uint8_t*) &(uint32_t) { 0x01020304 }, sizeof(uint32_t))); + ASSERT_OK(sd_dhcp_server_set_static_lease( + server, + &(struct in_addr) { .s_addr = 0x01020304 }, + (uint8_t *) &(uint32_t) { 0x01020304 }, + sizeof(uint32_t), + /* hostname= */ NULL)); /* Duplicated entry. */ - ASSERT_ERROR(sd_dhcp_server_set_static_lease(server, &(struct in_addr) { .s_addr = 0x01020304 }, - (uint8_t*) &(uint32_t) { 0x01020304 }, sizeof(uint32_t)), EEXIST); + ASSERT_ERROR(sd_dhcp_server_set_static_lease( + server, + &(struct in_addr) { .s_addr = 0x01020304 }, + (uint8_t *) &(uint32_t) { 0x01020304 }, + sizeof(uint32_t), + /* hostname= */ NULL), + EEXIST); /* Address is conflicted. */ - ASSERT_ERROR(sd_dhcp_server_set_static_lease(server, &(struct in_addr) { .s_addr = 0x01020304 }, - (uint8_t*) &(uint32_t) { 0x01020305 }, sizeof(uint32_t)), EEXIST); + ASSERT_ERROR(sd_dhcp_server_set_static_lease( + server, + &(struct in_addr) { .s_addr = 0x01020304 }, + (uint8_t *) &(uint32_t) { 0x01020305 }, + sizeof(uint32_t), + /* hostname= */ NULL), + EEXIST); /* Client ID is conflicted. */ - ASSERT_ERROR(sd_dhcp_server_set_static_lease(server, &(struct in_addr) { .s_addr = 0x01020305 }, - (uint8_t*) &(uint32_t) { 0x01020304 }, sizeof(uint32_t)), EEXIST); - - ASSERT_OK(sd_dhcp_server_set_static_lease(server, &(struct in_addr) { .s_addr = 0x01020305 }, - (uint8_t*) &(uint32_t) { 0x01020305 }, sizeof(uint32_t))); + ASSERT_ERROR(sd_dhcp_server_set_static_lease( + server, + &(struct in_addr) { .s_addr = 0x01020305 }, + (uint8_t *) &(uint32_t) { 0x01020304 }, + sizeof(uint32_t), + /* hostname= */ NULL), + EEXIST); + + ASSERT_OK(sd_dhcp_server_set_static_lease( + server, + &(struct in_addr) { .s_addr = 0x01020305 }, + (uint8_t *) &(uint32_t) { 0x01020305 }, + sizeof(uint32_t), + /* hostname= */ NULL)); /* Remove the previous entry. */ - ASSERT_OK(sd_dhcp_server_set_static_lease(server, &(struct in_addr) { .s_addr = 0x00000000 }, - (uint8_t*) &(uint32_t) { 0x01020305 }, sizeof(uint32_t))); + ASSERT_OK(sd_dhcp_server_set_static_lease( + server, + &(struct in_addr) { .s_addr = 0x00000000 }, + (uint8_t *) &(uint32_t) { 0x01020305 }, + sizeof(uint32_t), + /* hostname= */ NULL)); /* Then, set a different address. */ - ASSERT_OK(sd_dhcp_server_set_static_lease(server, &(struct in_addr) { .s_addr = 0x01020306 }, - (uint8_t*) &(uint32_t) { 0x01020305 }, sizeof(uint32_t))); + ASSERT_OK(sd_dhcp_server_set_static_lease( + server, + &(struct in_addr) { .s_addr = 0x01020306 }, + (uint8_t *) &(uint32_t) { 0x01020305 }, + sizeof(uint32_t), + /* hostname= */ NULL)); /* Remove again. */ - ASSERT_OK(sd_dhcp_server_set_static_lease(server, &(struct in_addr) { .s_addr = 0x00000000 }, - (uint8_t*) &(uint32_t) { 0x01020305 }, sizeof(uint32_t))); + ASSERT_OK(sd_dhcp_server_set_static_lease( + server, + &(struct in_addr) { .s_addr = 0x00000000 }, + (uint8_t *) &(uint32_t) { 0x01020305 }, + sizeof(uint32_t), + /* hostname= */ NULL)); /* Try to remove non-existent entry. */ - ASSERT_OK(sd_dhcp_server_set_static_lease(server, &(struct in_addr) { .s_addr = 0x00000000 }, - (uint8_t*) &(uint32_t) { 0x01020305 }, sizeof(uint32_t))); + ASSERT_OK(sd_dhcp_server_set_static_lease( + server, + &(struct in_addr) { .s_addr = 0x00000000 }, + (uint8_t *) &(uint32_t) { 0x01020305 }, + sizeof(uint32_t), + /* hostname= */ NULL)); /* Try to remove non-existent entry. */ - ASSERT_OK(sd_dhcp_server_set_static_lease(server, &(struct in_addr) { .s_addr = 0x00000000 }, - (uint8_t*) &(uint32_t) { 0x01020306 }, sizeof(uint32_t))); + ASSERT_OK(sd_dhcp_server_set_static_lease( + server, + &(struct in_addr) { .s_addr = 0x00000000 }, + (uint8_t *) &(uint32_t) { 0x01020306 }, + sizeof(uint32_t), + /* hostname= */ NULL)); } static void test_domain_name(void) { diff --git a/src/network/networkd-dhcp-server-static-lease.c b/src/network/networkd-dhcp-server-static-lease.c index 1d13c9e7b98..5a37626daeb 100644 --- a/src/network/networkd-dhcp-server-static-lease.c +++ b/src/network/networkd-dhcp-server-static-lease.c @@ -2,6 +2,7 @@ #include "alloc-util.h" #include "conf-parser.h" +#include "dns-domain.h" #include "ether-addr-util.h" #include "hashmap.h" #include "networkd-dhcp-server-static-lease.h" @@ -17,6 +18,7 @@ static DHCPStaticLease* dhcp_static_lease_free(DHCPStaticLease *static_lease) { config_section_free(static_lease->section); free(static_lease->client_id); + free(static_lease->hostname); return mfree(static_lease); } @@ -215,3 +217,55 @@ int config_parse_dhcp_static_lease_hwaddr( TAKE_PTR(lease); return 0; } + +int config_parse_dhcp_static_lease_hostname( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + _cleanup_(dhcp_static_lease_free_or_set_invalidp) DHCPStaticLease *lease = NULL; + Network *network = ASSERT_PTR(userdata); + int r; + + assert(filename); + assert(lvalue); + assert(rvalue); + + r = lease_new_static(network, filename, section_line, &lease); + if (r < 0) + return log_oom(); + + if (isempty(rvalue)) { + lease->hostname = mfree(lease->hostname); + TAKE_PTR(lease); + return 0; + } + + r = dns_name_is_valid_ldh(rvalue); + if (r < 0) + return log_syntax_parse_error(unit, filename, line, r, lvalue, rvalue); + if (r == 0) { + log_syntax(unit, + LOG_WARNING, + filename, + line, + 0, + "Invalid hostname for DHCPv4 static lease, ignoring assignment: %s", + rvalue); + return 0; + } + + r = free_and_strdup(&lease->hostname, rvalue); + if (r < 0) + return log_oom(); + + TAKE_PTR(lease); + return 0; +} diff --git a/src/network/networkd-dhcp-server-static-lease.h b/src/network/networkd-dhcp-server-static-lease.h index 11711a38a77..14ce8460303 100644 --- a/src/network/networkd-dhcp-server-static-lease.h +++ b/src/network/networkd-dhcp-server-static-lease.h @@ -13,9 +13,11 @@ typedef struct DHCPStaticLease { struct in_addr address; uint8_t *client_id; size_t client_id_size; + char *hostname; } DHCPStaticLease; void network_drop_invalid_static_leases(Network *network); CONFIG_PARSER_PROTOTYPE(config_parse_dhcp_static_lease_address); CONFIG_PARSER_PROTOTYPE(config_parse_dhcp_static_lease_hwaddr); +CONFIG_PARSER_PROTOTYPE(config_parse_dhcp_static_lease_hostname); diff --git a/src/network/networkd-dhcp-server.c b/src/network/networkd-dhcp-server.c index cb37da7a581..4b1dbe6eb94 100644 --- a/src/network/networkd-dhcp-server.c +++ b/src/network/networkd-dhcp-server.c @@ -733,7 +733,12 @@ static int dhcp4_server_configure(Link *link) { } HASHMAP_FOREACH(static_lease, link->network->dhcp_static_leases_by_section) { - r = sd_dhcp_server_set_static_lease(link->dhcp_server, &static_lease->address, static_lease->client_id, static_lease->client_id_size); + r = sd_dhcp_server_set_static_lease( + link->dhcp_server, + &static_lease->address, + static_lease->client_id, + static_lease->client_id_size, + static_lease->hostname); if (r < 0) return log_link_error_errno(link, r, "Failed to set DHCPv4 static lease for DHCP server: %m"); } diff --git a/src/network/networkd-json.c b/src/network/networkd-json.c index 3784995845b..cbb4255b9a5 100644 --- a/src/network/networkd-json.c +++ b/src/network/networkd-json.c @@ -1300,6 +1300,7 @@ static int dhcp6_client_append_json(Link *link, sd_json_variant **v) { static int dhcp_client_lease_append_json(Link *link, sd_json_variant **v) { _cleanup_(sd_json_variant_unrefp) sd_json_variant *w = NULL; usec_t lease_timestamp_usec = USEC_INFINITY, t1 = USEC_INFINITY, t2 = USEC_INFINITY; + const char *hostname = NULL; int r; assert(link); @@ -1320,10 +1321,16 @@ static int dhcp_client_lease_append_json(Link *link, sd_json_variant **v) { if (r < 0 && r != -ENODATA) return r; - r = sd_json_buildo(&w, - JSON_BUILD_PAIR_FINITE_USEC("LeaseTimestampUSec", lease_timestamp_usec), - JSON_BUILD_PAIR_FINITE_USEC("Timeout1USec", t1), - JSON_BUILD_PAIR_FINITE_USEC("Timeout2USec", t2)); + r = sd_dhcp_lease_get_hostname(link->dhcp_lease, &hostname); + if (r < 0 && r != -ENODATA) + return r; + + r = sd_json_buildo( + &w, + JSON_BUILD_PAIR_FINITE_USEC("LeaseTimestampUSec", lease_timestamp_usec), + JSON_BUILD_PAIR_FINITE_USEC("Timeout1USec", t1), + JSON_BUILD_PAIR_FINITE_USEC("Timeout2USec", t2), + JSON_BUILD_PAIR_STRING_NON_EMPTY("Hostname", hostname)); if (r < 0) return r; diff --git a/src/network/networkd-network-gperf.gperf b/src/network/networkd-network-gperf.gperf index 40b97300e65..2d0b7d40641 100644 --- a/src/network/networkd-network-gperf.gperf +++ b/src/network/networkd-network-gperf.gperf @@ -395,6 +395,7 @@ DHCPServer.RapidCommit, config_parse_bool, DHCPServer.PersistLeases, config_parse_dhcp_server_persist_leases, 0, offsetof(Network, dhcp_server_persist_leases) DHCPServerStaticLease.Address, config_parse_dhcp_static_lease_address, 0, 0 DHCPServerStaticLease.MACAddress, config_parse_dhcp_static_lease_hwaddr, 0, 0 +DHCPServerStaticLease.Hostname, config_parse_dhcp_static_lease_hostname, 0, 0 Bridge.Cost, config_parse_uint32, 0, offsetof(Network, cost) Bridge.UseBPDU, config_parse_tristate, 0, offsetof(Network, use_bpdu) Bridge.HairPin, config_parse_tristate, 0, offsetof(Network, hairpin) diff --git a/src/shared/varlink-io.systemd.Network.c b/src/shared/varlink-io.systemd.Network.c index a3bf42dcf96..f4623ad5763 100644 --- a/src/shared/varlink-io.systemd.Network.c +++ b/src/shared/varlink-io.systemd.Network.c @@ -351,7 +351,9 @@ static SD_VARLINK_DEFINE_STRUCT_TYPE( SD_VARLINK_FIELD_COMMENT("T1 timeout (lease renewal time) in microseconds"), SD_VARLINK_DEFINE_FIELD(Timeout1USec, SD_VARLINK_INT, SD_VARLINK_NULLABLE), SD_VARLINK_FIELD_COMMENT("T2 timeout (lease rebinding time) in microseconds"), - SD_VARLINK_DEFINE_FIELD(Timeout2USec, SD_VARLINK_INT, SD_VARLINK_NULLABLE)); + SD_VARLINK_DEFINE_FIELD(Timeout2USec, SD_VARLINK_INT, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("Hostname received from DHCP server"), + SD_VARLINK_DEFINE_FIELD(Hostname, SD_VARLINK_STRING, SD_VARLINK_NULLABLE)); static SD_VARLINK_DEFINE_STRUCT_TYPE( PrivateOption, diff --git a/src/systemd/sd-dhcp-server.h b/src/systemd/sd-dhcp-server.h index b188e1f8393..2069b9d2875 100644 --- a/src/systemd/sd-dhcp-server.h +++ b/src/systemd/sd-dhcp-server.h @@ -79,7 +79,12 @@ int sd_dhcp_server_set_smtp(sd_dhcp_server *server, const struct in_addr smtp[], int sd_dhcp_server_add_option(sd_dhcp_server *server, sd_dhcp_option *v); int sd_dhcp_server_add_vendor_option(sd_dhcp_server *server, sd_dhcp_option *v); -int sd_dhcp_server_set_static_lease(sd_dhcp_server *server, const struct in_addr *address, uint8_t *client_id, size_t client_id_size); +int sd_dhcp_server_set_static_lease( + sd_dhcp_server *server, + const struct in_addr *address, + uint8_t *client_id, + size_t client_id_size, + const char *hostname); int sd_dhcp_server_set_lease_file(sd_dhcp_server *server, int dir_fd, const char *path); int sd_dhcp_server_set_max_lease_time(sd_dhcp_server *server, uint64_t t); diff --git a/test/fuzz/fuzz-network-parser/dhcp-server-static-lease b/test/fuzz/fuzz-network-parser/dhcp-server-static-lease index e14e7c01f84..b0d94aba6ed 100644 --- a/test/fuzz/fuzz-network-parser/dhcp-server-static-lease +++ b/test/fuzz/fuzz-network-parser/dhcp-server-static-lease @@ -16,10 +16,12 @@ DNS=9.9.9.9 [DHCPServerStaticLease] MACAddress=12:34:56:78:9a:bc Address=10.1.1.2 +Hostname=testhost [DHCPServerStaticLease] MACAddress=12:34:56:78:9a:bc Address=10.1.1.3 +Hostname=device.example.com [DHCPServerStaticLease] Address=10.1.1.4 diff --git a/test/test-network/conf/25-dhcp-client-fqdn-hostname.network b/test/test-network/conf/25-dhcp-client-fqdn-hostname.network new file mode 100644 index 00000000000..437be574287 --- /dev/null +++ b/test/test-network/conf/25-dhcp-client-fqdn-hostname.network @@ -0,0 +1,11 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[Match] +Name=veth99 + +[Network] +DHCP=ipv4 +IPv6AcceptRA=no + +[Link] +# This MAC overrides the default to match the second static lease +MACAddress=92:12:01:87:11:19 diff --git a/test/test-network/conf/25-dhcp-client-simple-hostname.network b/test/test-network/conf/25-dhcp-client-simple-hostname.network new file mode 100644 index 00000000000..01e36c1fcfc --- /dev/null +++ b/test/test-network/conf/25-dhcp-client-simple-hostname.network @@ -0,0 +1,7 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[Match] +Name=veth99 + +[Network] +DHCP=ipv4 +IPv6AcceptRA=no diff --git a/test/test-network/conf/25-dhcp-server-static-hostname.network b/test/test-network/conf/25-dhcp-server-static-hostname.network new file mode 100644 index 00000000000..bc6a369e5de --- /dev/null +++ b/test/test-network/conf/25-dhcp-server-static-hostname.network @@ -0,0 +1,27 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[Match] +Name=veth-peer + +[Network] +Address=10.1.1.1/24 +DHCPServer=yes +IPv6AcceptRA=no + +[DHCPServer] +PoolOffset=100 +PoolSize=50 +DefaultLeaseTimeSec=60 + +# Scenario 1: Option 12 +# Matches veth99's default MAC (from 25-veth.netdev) +[DHCPServerStaticLease] +MACAddress=12:34:56:78:9a:bc +Address=10.1.1.200 +Hostname=simple-host + +# Scenario 2: Option 81 +# Matches the MAC set by 25-dhcp-client-fqdn-hostname.network +[DHCPServerStaticLease] +MACAddress=92:12:01:87:11:19 +Address=10.1.1.201 +Hostname=fqdn.example.com diff --git a/test/test-network/systemd-networkd-tests.py b/test/test-network/systemd-networkd-tests.py index 93ad5635002..32c7b590447 100755 --- a/test/test-network/systemd-networkd-tests.py +++ b/test/test-network/systemd-networkd-tests.py @@ -7340,6 +7340,32 @@ class NetworkdDHCPServerTests(unittest.TestCase, Utilities): self.assertIn('Address: 10.1.1.200 (DHCPv4 via 10.1.1.1)', output) self.assertRegex(output, 'DHCPv4 Client ID: IAID:[0-9a-z]*/DUID') + def test_dhcp_server_static_lease_hostname_simple(self): + copy_network_unit('25-veth.netdev', + '25-dhcp-client-simple-hostname.network', + '25-dhcp-server-static-hostname.network') + start_networkd() + self.wait_online('veth99:routable', 'veth-peer:routable') + + output = networkctl_json('veth99') + check_json(output) + print(output) + data = json.loads(output) + self.assertEqual(data['DHCPv4Client']['Lease']['Hostname'], 'simple-host') + + def test_dhcp_server_static_lease_hostname_fqdn(self): + copy_network_unit('25-veth.netdev', + '25-dhcp-client-fqdn-hostname.network', + '25-dhcp-server-static-hostname.network') + start_networkd() + self.wait_online('veth99:routable', 'veth-peer:routable') + + output = networkctl_json('veth99') + check_json(output) + print(output) + data = json.loads(output) + self.assertEqual(data['DHCPv4Client']['Lease']['Hostname'], 'fqdn.example.com') + class NetworkdDHCPServerRelayAgentTests(unittest.TestCase, Utilities): def setUp(self):