]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
network: add DHCP server domain name option support (#39260)
authorGovind Venugopal <gvenugo3@asu.edu>
Wed, 15 Oct 2025 09:20:41 +0000 (02:20 -0700)
committerGitHub <noreply@github.com>
Wed, 15 Oct 2025 09:20:41 +0000 (11:20 +0200)
Implements DHCP option 15 (Domain Name) for systemd-networkd's DHCP
server, allowing administrators to configure the DNS default domain that
clients should use.

This addresses the feature request in issue #37077, where users needed
to manually configure domain names using
SendOption=15:string:example.com as a workaround.

This adds two new configuration options to the [DHCPServer] section:
- EmitDomain= (boolean): whether to send domain name to clients
- Domain= (string): the domain name to send (e.g., "example.com")

Example configuration:
  [DHCPServer] EmitDomain=yes Domain=example.com

This eliminates the need for manual workarounds using
SendOption=15:string:...

Fixes #37077

man/systemd.network.xml
src/libsystemd-network/dhcp-server-internal.h
src/libsystemd-network/sd-dhcp-server.c
src/libsystemd-network/test-dhcp-server.c
src/network/networkd-dhcp-server.c
src/network/networkd-network-gperf.gperf
src/network/networkd-network.c
src/network/networkd-network.h
src/systemd/sd-dhcp-server.h

index eefaa5572c24e8eb0430b45d01ae4c65917e4f7f..370ff3f969fc3deaf481494692a838553fafd64c 100644 (file)
@@ -3992,6 +3992,34 @@ ServerAddress=192.168.0.1/24</programlisting>
         <xi:include href="version-info.xml" xpointer="v226"/></listitem>
       </varlistentry>
 
+      <varlistentry>
+        <term><varname>EmitDomain=</varname></term>
+
+        <listitem><para>Takes a boolean. Configures whether the DHCP leases handed out
+        to clients shall contain domain name information (DHCP option 15). Defaults to
+        <literal>no</literal>.</para>
+
+        <xi:include href="version-info.xml" xpointer="v259"/></listitem>
+      </varlistentry>
+
+      <varlistentry>
+        <term><varname>Domain=</varname></term>
+
+        <listitem><para>Takes a domain name (such as <literal>example.com</literal>)
+        to pass to DHCP clients. This configures the DNS default domain for DHCP clients.
+        When set, DHCP clients will use this as their DNS search domain.</para>
+
+        <para>When <varname>EmitDomain=yes</varname> is set but <varname>Domain=</varname>
+        is not configured, the domain name will be automatically derived from the system's
+        fully qualified hostname. For example, if the system's hostname is
+        <literal>host.example.com</literal>, the domain <literal>example.com</literal>
+        will be sent to clients. If the system's hostname does not contain a domain part
+        (e.g., hostname is just <literal>host</literal>), no domain name will be sent to
+        DHCP clients. When empty or unset, defaults to no domain name.</para>
+
+        <xi:include href="version-info.xml" xpointer="v259"/></listitem>
+      </varlistentry>
+
       <varlistentry>
         <term><varname>BootServerAddress=</varname></term>
 
index 1da18cce6031e18e8ba41fd1ff44ee090139549a..eeaed11096b84c832513da8307aabc034f44ca95 100644 (file)
@@ -46,6 +46,7 @@ typedef struct sd_dhcp_server {
         uint32_t pool_size;
 
         char *timezone;
+        char *domain_name;
 
         DHCPServerData servers[_SD_DHCP_LEASE_SERVER_TYPE_MAX];
         struct in_addr boot_server_address;
index 335f01f1645aa149fa678b54bfebc988f5ebce4e..43c7c241758e526a1a75767a13c621b840b003ba 100644 (file)
@@ -128,6 +128,7 @@ static sd_dhcp_server *dhcp_server_free(sd_dhcp_server *server) {
         free(server->boot_server_name);
         free(server->boot_filename);
         free(server->timezone);
+        free(server->domain_name);
 
         for (sd_dhcp_lease_server_type_t i = 0; i < _SD_DHCP_LEASE_SERVER_TYPE_MAX; i++)
                 free(server->servers[i].addr);
@@ -625,6 +626,15 @@ static int server_send_offer_or_ack(
                         return r;
         }
 
+        if (server->domain_name) {
+                r = dhcp_option_append(
+                                &packet->dhcp, req->max_optlen, &offset, 0,
+                                SD_DHCP_OPTION_DOMAIN_NAME,
+                                strlen(server->domain_name), server->domain_name);
+                if (r < 0)
+                        return r;
+        }
+
         /* RFC 8925 section 3.3. DHCPv4 Server Behavior
          * The server MUST NOT include the IPv6-Only Preferred option in the DHCPOFFER or DHCPACK message if
          * the option was not present in the Parameter Request List sent by the client. */
@@ -1415,6 +1425,22 @@ int sd_dhcp_server_set_timezone(sd_dhcp_server *server, const char *tz) {
         return 1;
 }
 
+int sd_dhcp_server_set_domain_name(sd_dhcp_server *server, const char *domain_name) {
+        int r;
+
+        assert_return(server, -EINVAL);
+
+        if (domain_name) {
+                r = dns_name_is_valid(domain_name);
+                if (r < 0)
+                        return r;
+                if (r == 0)
+                        return -EINVAL;
+        }
+
+        return free_and_strdup(&server->domain_name, domain_name);
+}
+
 int sd_dhcp_server_set_max_lease_time(sd_dhcp_server *server, uint64_t t) {
         assert_return(server, -EINVAL);
 
index 61189fe5452ec96ae02d2b4316e901b8ceeabfb2..17c5cddd541e1df5d862d46376fbfbc80258e3b8 100644 (file)
@@ -316,6 +316,44 @@ static void test_static_lease(void) {
                                                   (uint8_t*) &(uint32_t) { 0x01020306 }, sizeof(uint32_t)));
 }
 
+static void test_domain_name(void) {
+        _cleanup_(sd_dhcp_server_unrefp) sd_dhcp_server *server = NULL;
+
+        log_debug("/* %s */", __func__);
+
+        ASSERT_OK(sd_dhcp_server_new(&server, 1));
+
+        /* Test setting domain name */
+        ASSERT_OK_POSITIVE(sd_dhcp_server_set_domain_name(server, "example.com"));
+
+        /* Test setting same domain name (should return 0 - no change) */
+        ASSERT_OK_ZERO(sd_dhcp_server_set_domain_name(server, "example.com"));
+
+        /* Test changing domain name */
+        ASSERT_OK_POSITIVE(sd_dhcp_server_set_domain_name(server, "test.local"));
+
+        /* Test clearing domain name */
+        ASSERT_OK_POSITIVE(sd_dhcp_server_set_domain_name(server, NULL));
+
+        /* Test clearing again (should return 0 - already cleared) */
+        ASSERT_OK_ZERO(sd_dhcp_server_set_domain_name(server, NULL));
+
+        /* Test invalid domain name */
+        ASSERT_ERROR(sd_dhcp_server_set_domain_name(server, "invalid..domain"), EINVAL);
+
+        /* Test empty string (treated differently from NULL) */
+        ASSERT_OK_POSITIVE(sd_dhcp_server_set_domain_name(server, ""));
+
+        /* Test clearing domain name with NULL */
+        ASSERT_OK_POSITIVE(sd_dhcp_server_set_domain_name(server, NULL));
+
+        /* Test valid domain with subdomain */
+        ASSERT_OK_POSITIVE(sd_dhcp_server_set_domain_name(server, "sub.example.com"));
+
+        /* Test single-label domain */
+        ASSERT_OK_POSITIVE(sd_dhcp_server_set_domain_name(server, "local"));
+}
+
 int main(int argc, char *argv[]) {
         int r;
 
@@ -323,6 +361,7 @@ int main(int argc, char *argv[]) {
 
         test_client_id_hash();
         test_static_lease();
+        test_domain_name();
 
         r = test_basic(true);
         if (r < 0)
index e36d80e03fcd7efafbbb70be6c8c3c998dc9bef7..f27cb28cefd90a31f19cd3c9c7e71c43d4070825 100644 (file)
@@ -13,6 +13,7 @@
 #include "fd-util.h"
 #include "fileio.h"
 #include "hashmap.h"
+#include "hostname-setup.h"
 #include "network-common.h"
 #include "networkd-address.h"
 #include "networkd-dhcp-server.h"
 #include "string-util.h"
 #include "strv.h"
 
+static int get_hostname_domain(char **ret) {
+        _cleanup_free_ char *hostname = NULL;
+        const char *domain;
+        int r;
+
+        assert(ret);
+
+        /* Get the full hostname (FQDN if available) */
+        r = gethostname_full(GET_HOSTNAME_ALLOW_LOCALHOST | GET_HOSTNAME_FALLBACK_DEFAULT, &hostname);
+        if (r < 0)
+                return r;
+
+        /* Find the first dot to extract the domain part */
+        domain = strchr(hostname, '.');
+        if (!domain)
+                return -ENOENT;  /* No domain part in hostname */
+
+        domain++;  /* Skip the dot */
+        if (isempty(domain))
+                return -ENOENT;  /* Empty domain after dot */
+
+        return strdup_to(ret, domain);
+}
+
 static bool link_dhcp4_server_enabled(Link *link) {
         assert(link);
 
@@ -678,6 +703,29 @@ static int dhcp4_server_configure(Link *link) {
                 }
         }
 
+        if (link->network->dhcp_server_emit_domain) {
+                _cleanup_free_ char *buffer = NULL;
+                const char *domain = NULL;
+
+                if (link->network->dhcp_server_domain)
+                        domain = link->network->dhcp_server_domain;
+                else {
+                        r = get_hostname_domain(&buffer);
+                        if (r < 0)
+                                log_link_warning_errno(link, r, "Failed to determine domain name from host's hostname, will not send domain in DHCP leases: %m");
+                        else {
+                                domain = buffer;
+                                log_link_debug(link, "Using autodetected domain name '%s' for DHCP server.", domain);
+                        }
+                }
+
+                if (domain) {
+                        r = sd_dhcp_server_set_domain_name(link->dhcp_server, domain);
+                        if (r < 0)
+                                return log_link_error_errno(link, r, "Failed to set domain name for DHCP server: %m");
+                }
+        }
+
         ORDERED_HASHMAP_FOREACH(p, link->network->dhcp_server_send_options) {
                 r = sd_dhcp_server_add_option(link->dhcp_server, p);
                 if (r == -EEXIST)
index b9e3fc6a290c011e4e40dad8957f1cb939796d38..40b97300e652225a6f2177c48961f7c9dced3804 100644 (file)
@@ -381,6 +381,8 @@ DHCPServer.EmitRouter,                           config_parse_bool,
 DHCPServer.Router,                               config_parse_in_addr_non_null,                  AF_INET,                                offsetof(Network, dhcp_server_router)
 DHCPServer.EmitTimezone,                         config_parse_bool,                              0,                                      offsetof(Network, dhcp_server_emit_timezone)
 DHCPServer.Timezone,                             config_parse_timezone,                          0,                                      offsetof(Network, dhcp_server_timezone)
+DHCPServer.EmitDomain,                           config_parse_bool,                              0,                                      offsetof(Network, dhcp_server_emit_domain)
+DHCPServer.Domain,                               config_parse_dns_name,                          0,                                      offsetof(Network, dhcp_server_domain)
 DHCPServer.PoolOffset,                           config_parse_uint32,                            0,                                      offsetof(Network, dhcp_server_pool_offset)
 DHCPServer.PoolSize,                             config_parse_uint32,                            0,                                      offsetof(Network, dhcp_server_pool_size)
 DHCPServer.SendVendorOption,                     config_parse_dhcp_send_option,                  0,                                      offsetof(Network, dhcp_server_send_vendor_options)
index e27fabea81a89fc2b06d4fd4455e372094f7cdee..4e8566afa675b0089a4297fc4faf0f3209ecb3b2 100644 (file)
@@ -755,6 +755,7 @@ static Network *network_free(Network *network) {
         free(network->dhcp_server_boot_server_name);
         free(network->dhcp_server_boot_filename);
         free(network->dhcp_server_timezone);
+        free(network->dhcp_server_domain);
         free(network->dhcp_server_uplink_name);
         for (sd_dhcp_lease_server_type_t t = 0; t < _SD_DHCP_LEASE_SERVER_TYPE_MAX; t++)
                 free(network->dhcp_server_emit[t].addresses);
index 677d337e0f219d5405d035b39df5f4c7360e4f15..edd2177dd3bf4821e55023f7d384b7fc0fb6be57 100644 (file)
@@ -220,6 +220,8 @@ typedef struct Network {
         struct in_addr dhcp_server_router;
         bool dhcp_server_emit_timezone;
         char *dhcp_server_timezone;
+        bool dhcp_server_emit_domain;
+        char *dhcp_server_domain;
         usec_t dhcp_server_default_lease_time_usec, dhcp_server_max_lease_time_usec;
         uint32_t dhcp_server_pool_offset;
         uint32_t dhcp_server_pool_size;
index d6940ac7f8657ecb3fa36db401b511501227f837..b188e1f83935f9a8d43056f5116614716497c9ee 100644 (file)
@@ -61,6 +61,7 @@ int sd_dhcp_server_set_boot_server_name(sd_dhcp_server *server, const char *name
 int sd_dhcp_server_set_boot_filename(sd_dhcp_server *server, const char *filename);
 int sd_dhcp_server_set_bind_to_interface(sd_dhcp_server *server, int enabled);
 int sd_dhcp_server_set_timezone(sd_dhcp_server *server, const char *timezone);
+int sd_dhcp_server_set_domain_name(sd_dhcp_server *server, const char *domain_name);
 int sd_dhcp_server_set_router(sd_dhcp_server *server, const struct in_addr *address);
 
 int sd_dhcp_server_set_servers(