]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
sd-dhcp-server: Add Hostname= option to static leases
authorChris Down <chris@chrisdown.name>
Sun, 9 Nov 2025 16:59:59 +0000 (00:59 +0800)
committerYu Watanabe <watanabe.yu+github@gmail.com>
Thu, 13 Nov 2025 22:50:13 +0000 (07:50 +0900)
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
18 files changed:
man/systemd.network.xml
src/libsystemd-network/fuzz-dhcp-server.c
src/libsystemd-network/sd-dhcp-lease.c
src/libsystemd-network/sd-dhcp-server-lease.c
src/libsystemd-network/sd-dhcp-server.c
src/libsystemd-network/test-dhcp-server.c
src/network/networkd-dhcp-server-static-lease.c
src/network/networkd-dhcp-server-static-lease.h
src/network/networkd-dhcp-server.c
src/network/networkd-json.c
src/network/networkd-network-gperf.gperf
src/shared/varlink-io.systemd.Network.c
src/systemd/sd-dhcp-server.h
test/fuzz/fuzz-network-parser/dhcp-server-static-lease
test/test-network/conf/25-dhcp-client-fqdn-hostname.network [new file with mode: 0644]
test/test-network/conf/25-dhcp-client-simple-hostname.network [new file with mode: 0644]
test/test-network/conf/25-dhcp-server-static-hostname.network [new file with mode: 0644]
test/test-network/systemd-networkd-tests.py

index 4710c7fe7fd5105d765ddf431df0429c58ce67a5..a75f89de99d40745690bcab5c90f08ec631d7d0f 100644 (file)
@@ -4227,6 +4227,25 @@ ServerAddress=192.168.0.1/24</programlisting>
 
         <xi:include href="version-info.xml" xpointer="v249"/></listitem>
       </varlistentry>
+
+      <varlistentry>
+        <term><varname>Hostname=</varname></term>
+
+        <listitem><para>The hostname to send to the client in DHCP replies. This can be either a simple
+        hostname (e.g., <literal>mydevice</literal>) or a fully qualified domain name (e.g.,
+        <literal>mydevice.example.com</literal>), 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.</para>
+
+        <para>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.</para>
+
+        <xi:include href="version-info.xml" xpointer="v259"/></listitem>
+      </varlistentry>
     </variablelist>
   </refsect1>
 
index 47e3c9efa9791953858a30e4ae24ce4c946a4478..a91964fe0df00777222f20c365b2687e97b3810c 100644 (file)
@@ -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) {
index a98ec7fc63b0f0d1b3cba0f6581ec2d8b67d6b86..fcf732bec447b302f34d9acf034295ba1a202726 100644 (file)
@@ -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;
 
index 1daafde7164f41a60891bf44a25aa0b07912fc54..68b3ac5bccd605027bb0e681683f8ed1ed4a03dd 100644 (file)
@@ -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;
index 3a4d3aa202806e217bcec3a7ce9024db0a2ad1e4..7503160643617201091f74551285d4291817c6d4 100644 (file)
@@ -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);
 }
 
index 705c0e92beb358ac38b2983c1510ff56f9982ec0..7789037011e7d3ab1a3ddb9833481a7fafaa5aa4 100644 (file)
@@ -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) {
index 1d13c9e7b9862d37ed536b7e940f525f16488e37..5a37626daeb36c18d7ad065890edfe9e66e97484 100644 (file)
@@ -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;
+}
index 11711a38a773cf06507623034a2d8bda347d0a73..14ce8460303c990335fa124d3f1102c1f9fc9b5d 100644 (file)
@@ -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);
index cb37da7a5815aae43b47aa42895c35ba26bbc64a..4b1dbe6eb948b7c94f75edfcead92aee539ac872 100644 (file)
@@ -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");
         }
index 3784995845bc0c0f4d79c6b8b6feda857b137d48..cbb4255b9a586a3bc02b77d129b91a38efabe51b 100644 (file)
@@ -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;
 
index 40b97300e652225a6f2177c48961f7c9dced3804..2d0b7d40641a7759e83fc5e61998b93f2dbef43c 100644 (file)
@@ -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)
index a3bf42dcf963cab26b0bddc4911a715cda1631a1..f4623ad5763ec398badea90d15ab81bb0345f220 100644 (file)
@@ -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,
index b188e1f83935f9a8d43056f5116614716497c9ee..2069b9d2875c56fa18994bf325d5521d286016ed 100644 (file)
@@ -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);
index e14e7c01f8414ccd8ece7c56290608baba02b18e..b0d94aba6ed0729d8dc0acb53cad540b5408d4f5 100644 (file)
@@ -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 (file)
index 0000000..437be57
--- /dev/null
@@ -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 (file)
index 0000000..01e36c1
--- /dev/null
@@ -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 (file)
index 0000000..bc6a369
--- /dev/null
@@ -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
index 93ad5635002d0e0bef80e3a88f27ff65bb93c9d9..32c7b5904477e4c839be1507911bb245821f050f 100755 (executable)
@@ -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):