network: support the RFC 4833 timezone option in DHCPv6

For Legacy IP, DHCPv4 has supported UseTimezone= since v226, requesting
the tzdb timezone name (option 101) and passing it to timedated. The
equivalent exists for DHCPv6 (option 42; OPTION_NEW_TZDB_TIMEZONE), but
support for that was missing.

Parse option 42 into the lease, validated with timezone_is_valid() as
on the DHCPv4 side, expose it as sd_dhcp6_lease_get_timezone(), and add
UseTimezone= to the [DHCPv6] section. The timezone is applied on both
the address-acquired and information-request paths; the latter matters
because a SLAAC network with the O flag set only ever does stateless
DHCPv6.

The test message uses "UTC" rather than a real tzdb name because
timezone_is_valid() wants to open the tzfile, and UTC is the one name
that is accepted even when no tzdata is installed.
This commit is contained in:
David Woodhouse authored and Yu Watanabe committed 2026-11-02 07:36:47 +09:00
1 parent b43258ee75
commit 16a7a07e9d
11 files changed
+194 -9

No files matched your search

+10
View File
@@ -3322,6 +3322,16 @@ MultiPathRoute=2001:db8::1@eth0</programlisting>
</listitem>
</varlistentry>
<varlistentry>
<term><varname>UseTimezone=</varname></term>
<listitem>
<para>When true, the timezone received from the DHCPv6 server will be set as timezone
of the local system. Defaults to false.</para>
<xi:include href="version-info.xml" xpointer="v263"/>
</listitem>
</varlistentry>
<varlistentry>
<term><varname>UseDelegatedPrefix=</varname></term>
<listitem>
@@ -48,6 +48,7 @@ struct sd_dhcp6_lease {
char **sip_domains;
char *fqdn;
char *captive_portal;
char *timezone;
struct sd_dhcp6_option **sorted_vendor_options;
Set *vendor_options;
};
@@ -69,6 +70,7 @@ int dhcp6_lease_add_sip_addrs(sd_dhcp6_lease *lease, const uint8_t *optval, size
int dhcp6_lease_add_sip_domains(sd_dhcp6_lease *lease, const uint8_t *optval, size_t optlen);
int dhcp6_lease_set_fqdn(sd_dhcp6_lease *lease, const uint8_t *optval, size_t optlen);
int dhcp6_lease_set_captive_portal(sd_dhcp6_lease *lease, const uint8_t *optval, size_t optlen);
int dhcp6_lease_set_timezone(sd_dhcp6_lease *lease, const uint8_t *optval, size_t optlen);
int dhcp6_lease_new(sd_dhcp6_lease **ret);
int dhcp6_lease_new_from_message(
+37
View File
@@ -15,6 +15,7 @@
#include "sort-util.h"
#include "string-util.h"
#include "strv.h"
#include "time-util.h"
#include "unaligned.h"
#define IRT_DEFAULT (1 * USEC_PER_DAY)
@@ -740,6 +741,34 @@ int sd_dhcp6_lease_get_captive_portal(sd_dhcp6_lease *lease, const char **ret) {
return 0;
}
int dhcp6_lease_set_timezone(sd_dhcp6_lease *lease, const uint8_t *optval, size_t optlen) {
_cleanup_free_ char *tz = NULL;
int r;
assert(lease);
assert(optval || optlen == 0);
r = dhcp6_option_parse_string(optval, optlen, &tz);
if (r < 0)
return r;
if (!tz || !timezone_is_valid(tz, LOG_DEBUG))
return -EINVAL;
return free_and_replace(lease->timezone, tz);
}
int sd_dhcp6_lease_get_timezone(sd_dhcp6_lease *lease, const char **ret) {
assert_return(lease, -EINVAL);
assert_return(ret, -EINVAL);
if (!lease->timezone)
return -ENODATA;
*ret = lease->timezone;
return 0;
}
int sd_dhcp6_lease_get_vendor_options(sd_dhcp6_lease *lease, sd_dhcp6_option ***ret) {
int r;
@@ -1006,6 +1035,13 @@ static int dhcp6_lease_parse_message(
break;
case SD_DHCP6_OPTION_TZDB_TIMEZONE:
r = dhcp6_lease_set_timezone(lease, optval, optlen);
if (r < 0)
log_dhcp6_client_errno(client, r, "Failed to parse timezone option, ignoring: %m");
break;
case SD_DHCP6_OPTION_INFORMATION_REFRESH_TIME:
if (optlen != 4)
return log_dhcp6_client_errno(client, SYNTHETIC_ERRNO(EINVAL),
@@ -1079,6 +1115,7 @@ static sd_dhcp6_lease *dhcp6_lease_free(sd_dhcp6_lease *lease) {
dns_resolver_free_array(lease->dnr, lease->n_dnr);
free(lease->fqdn);
free(lease->captive_portal);
free(lease->timezone);
strv_free(lease->domains);
free(lease->ntp);
strv_free(lease->ntp_fqdn);
@@ -403,6 +403,25 @@ TEST(option_status) {
ASSERT_NULL(ia);
}
TEST(lease_timezone) {
_cleanup_(sd_dhcp6_lease_unrefp) sd_dhcp6_lease *lease = NULL;
const char *str;
ASSERT_OK(dhcp6_lease_new(&lease));
/* "UTC" in the main test messages bypasses the tzfile check in
* verify_timezone(); exercise the validation with invalid inputs. */
ASSERT_ERROR(dhcp6_lease_set_timezone(lease, (const uint8_t *) "", 0), EINVAL);
ASSERT_ERROR(dhcp6_lease_set_timezone(lease, (const uint8_t *) "UTC\0", 4), EINVAL);
ASSERT_ERROR(dhcp6_lease_set_timezone(lease, (const uint8_t *) "/etc/passwd", 11), EINVAL);
ASSERT_ERROR(dhcp6_lease_set_timezone(lease, (const uint8_t *) "../passwd", 9), EINVAL);
ASSERT_ERROR(sd_dhcp6_lease_get_timezone(lease, &str), ENODATA);
ASSERT_OK(dhcp6_lease_set_timezone(lease, (const uint8_t *) "UTC", 3));
ASSERT_OK(sd_dhcp6_lease_get_timezone(lease, &str));
ASSERT_STREQ(str, "UTC");
}
TEST(client_parse_message_issue_22099) {
static const uint8_t msg[] = {
/* Message type */
@@ -782,6 +801,9 @@ static const uint8_t msg_reply[] = {
/* Client FQDN */
0x00, SD_DHCP6_OPTION_CLIENT_FQDN, 0x00, 0x13,
0x01, 0x06, 'c', 'l', 'i', 'e', 'n', 't', 0x03, 'l', 'a', 'b', 0x05, 'i', 'n', 't', 'r', 'a', 0x00,
/* Time Zone (TZ Database name) */
0x00, SD_DHCP6_OPTION_TZDB_TIMEZONE, 0x00, 0x03,
'U', 'T', 'C',
/* Vendor specific options */
0x00, SD_DHCP6_OPTION_VENDOR_OPTS, 0x00, 0x09,
0x00, 0x00, 0x00, 0x20, 0x00, 0xf7, 0x00, 0x01, VENDOR_SUBOPTION_BYTES,
@@ -871,6 +893,9 @@ static const uint8_t msg_advertise[] = {
/* Client FQDN */
0x00, SD_DHCP6_OPTION_CLIENT_FQDN, 0x00, 0x13,
0x01, 0x06, 'c', 'l', 'i', 'e', 'n', 't', 0x03, 'l', 'a', 'b', 0x05, 'i', 'n', 't', 'r', 'a', 0x00,
/* Time Zone (TZ Database name) */
0x00, SD_DHCP6_OPTION_TZDB_TIMEZONE, 0x00, 0x03,
'U', 'T', 'C',
/* Vendor specific options */
0x00, SD_DHCP6_OPTION_VENDOR_OPTS, 0x00, 0x09,
0x00, 0x00, 0x00, 0x20, 0x00, 0xf7, 0x00, 0x01, VENDOR_SUBOPTION_BYTES,
@@ -970,6 +995,9 @@ static void test_lease_common(sd_dhcp6_client *client) {
ASSERT_OK(sd_dhcp6_lease_get_fqdn(lease, &str));
ASSERT_STREQ(str, "client.lab.intra");
ASSERT_OK(sd_dhcp6_lease_get_timezone(lease, &str));
ASSERT_STREQ(str, "UTC");
ASSERT_OK_EQ(sd_dhcp6_lease_get_dns(lease, &addrs), 2);
ASSERT_TRUE(in6_addr_equal(&addrs[0], &dns1));
ASSERT_TRUE(in6_addr_equal(&addrs[1], &dns2));
+37
View File
@@ -311,6 +311,29 @@ static int dhcp6_request_hostname(Link *link) {
return 0;
}
static int dhcp6_request_timezone(Link *link) {
const char *tz;
int r;
assert(link);
assert(link->network);
if (!link->network->dhcp6_use_timezone)
return 0;
r = sd_dhcp6_lease_get_timezone(link->dhcp6_lease, &tz);
if (r == -ENODATA)
return 0;
if (r < 0)
return r;
r = manager_set_timezone(link->manager, tz);
if (r < 0)
log_link_warning_errno(link, r, "Failed to set timezone to '%s', ignoring: %m", tz);
return 0;
}
static int dhcp6_lease_ip_acquired(sd_dhcp6_client *client, Link *link) {
_cleanup_(sd_dhcp6_lease_unrefp) sd_dhcp6_lease *lease_old = NULL;
sd_dhcp6_lease *lease;
@@ -329,6 +352,10 @@ static int dhcp6_lease_ip_acquired(sd_dhcp6_client *client, Link *link) {
if (r < 0)
return r;
r = dhcp6_request_timezone(link);
if (r < 0)
return r;
r = dhcp6_address_acquired(link);
if (r < 0)
return r;
@@ -370,6 +397,10 @@ static int dhcp6_lease_information_acquired(sd_dhcp6_client *client, Link *link)
unref_and_replace_new_ref(link->dhcp6_lease, lease, sd_dhcp6_lease_ref, sd_dhcp6_lease_unref);
r = dhcp6_request_timezone(link);
if (r < 0)
return r;
link_dirty(link);
return 0;
}
@@ -690,6 +721,12 @@ static int dhcp6_configure(Link *link) {
return log_link_debug_errno(link, r, "DHCPv6 CLIENT: Failed to request captive portal: %m");
}
if (link->network->dhcp6_use_timezone) {
r = sd_dhcp6_client_set_request_option(client, SD_DHCP6_OPTION_TZDB_TIMEZONE);
if (r < 0)
return log_link_debug_errno(link, r, "DHCPv6 CLIENT: Failed to request timezone: %m");
}
if (link_get_use_ntp(link, NETWORK_CONFIG_SOURCE_DHCP6)) {
r = sd_dhcp6_client_set_request_option(client, SD_DHCP6_OPTION_NTP_SERVER);
if (r < 0)
+36 -9
View File
@@ -90,6 +90,8 @@ static int match_prepare_for_sleep(sd_bus_message *message, void *userdata, sd_b
return 0;
}
static int manager_send_timezone(Manager *m);
static int on_connected(sd_bus_message *message, void *userdata, sd_bus_error *ret_error) {
Manager *m = ASSERT_PTR(userdata);
@@ -99,7 +101,7 @@ static int on_connected(sd_bus_message *message, void *userdata, sd_bus_error *r
if (m->dynamic_hostname)
(void) manager_set_hostname(m, m->dynamic_hostname);
if (m->dynamic_timezone)
(void) manager_set_timezone(m, m->dynamic_timezone);
(void) manager_send_timezone(m);
if (m->product_uuid_requested)
(void) manager_request_product_uuid(m);
@@ -1302,6 +1304,7 @@ int manager_set_hostname(Manager *m, const char *hostname) {
}
static int set_timezone_handler(sd_bus_message *m, void *userdata, sd_bus_error *ret_error) {
Manager *manager = ASSERT_PTR(userdata);
const sd_bus_error *e;
int r;
@@ -1311,21 +1314,19 @@ static int set_timezone_handler(sd_bus_message *m, void *userdata, sd_bus_error
if (e) {
r = sd_bus_error_get_errno(e);
log_warning_errno(r, "Could not set timezone: %s", bus_error_message(e, r));
/* Forget the cached zone, so that the next lease retries. */
manager->dynamic_timezone = mfree(manager->dynamic_timezone);
}
return 1;
}
int manager_set_timezone(Manager *m, const char *tz) {
static int manager_send_timezone(Manager *m) {
int r;
assert(m);
assert(tz);
log_debug("Setting system timezone: '%s'", tz);
r = free_and_strdup_warn(&m->dynamic_timezone, tz);
if (r < 0)
return r;
assert(m->dynamic_timezone);
if (sd_bus_is_ready(m->bus) <= 0) {
log_debug("Not connected to system bus, setting system timezone later.");
@@ -1340,7 +1341,7 @@ int manager_set_timezone(Manager *m, const char *tz) {
set_timezone_handler,
m,
"sb",
tz,
m->dynamic_timezone,
false);
if (r < 0)
return log_error_errno(r, "Could not set timezone: %m");
@@ -1348,6 +1349,32 @@ int manager_set_timezone(Manager *m, const char *tz) {
return 0;
}
int manager_set_timezone(Manager *m, const char *tz) {
int r;
assert(m);
assert(tz);
/* DHCP leases are renewed periodically; do not reset the timezone (and
* override a possible manual change) unless it actually changed. */
if (streq_ptr(m->dynamic_timezone, tz))
return 0;
log_debug("Setting system timezone: '%s'", tz);
r = free_and_strdup_warn(&m->dynamic_timezone, tz);
if (r < 0)
return r;
r = manager_send_timezone(m);
if (r < 0) {
/* Forget the cached zone, so that the next lease retries. */
m->dynamic_timezone = mfree(m->dynamic_timezone);
return r;
}
return 0;
}
int manager_reload(Manager *m, sd_bus_message *message, sd_varlink *varlink, bool reconfigure_links) {
Link *link;
int r;
+1
View File
@@ -318,6 +318,7 @@ DHCPv6.UseDomains, config_parse_use_domains,
DHCPv6.UseNTP, config_parse_tristate, 0, offsetof(Network, dhcp6_use_ntp)
DHCPv6.UseSIP, config_parse_bool, 0, offsetof(Network, dhcp6_use_sip)
DHCPv6.UseCaptivePortal, config_parse_bool, 0, offsetof(Network, dhcp6_use_captive_portal)
DHCPv6.UseTimezone, config_parse_bool, 0, offsetof(Network, dhcp6_use_timezone)
DHCPv6.MUDURL, config_parse_mud_url, 0, offsetof(Network, dhcp6_mudurl)
DHCPv6.SendHostname, config_parse_dhcp_send_hostname, AF_INET6, 0
DHCPv6.SendHostnameUpdates, config_parse_bool, 0, offsetof(Network, dhcp6_send_hostname_updates)
+1
View File
@@ -188,6 +188,7 @@ typedef struct Network {
int dhcp6_use_ntp;
bool dhcp6_use_sip;
bool dhcp6_use_captive_portal;
bool dhcp6_use_timezone;
bool dhcp6_use_rapid_commit;
UseDomains dhcp6_use_domains;
uint32_t dhcp6_iaid;
+1
View File
@@ -81,6 +81,7 @@ int sd_dhcp6_lease_get_sip_addrs(sd_dhcp6_lease *lease, const struct in6_addr **
int sd_dhcp6_lease_get_sip_domains(sd_dhcp6_lease *lease, char ***ret);
int sd_dhcp6_lease_get_fqdn(sd_dhcp6_lease *lease, const char **ret);
int sd_dhcp6_lease_get_captive_portal(sd_dhcp6_lease *lease, const char **ret);
int sd_dhcp6_lease_get_timezone(sd_dhcp6_lease *lease, const char **ret);
int sd_dhcp6_lease_get_vendor_options(sd_dhcp6_lease *lease, sd_dhcp6_option ***ret);
_SD_DECLARE_TRIVIAL_REF_UNREF_FUNC(sd_dhcp6_lease);
@@ -0,0 +1,9 @@
# SPDX-License-Identifier: LGPL-2.1-or-later
[Match]
Name=veth99
[Network]
DHCP=ipv6
[DHCPv6]
UseTimezone=yes
@@ -8832,6 +8832,38 @@ class NetworkdDHCPClientTests(unittest.TestCase, Utilities):
def tearDown(self):
tear_down_common()
def test_dhcp_client_ipv6_use_timezone(self):
copy_network_unit(
'25-veth.netdev',
'25-dhcp-server-veth-peer.network',
'25-dhcp-client-ipv6-timezone.network',
)
# Make sure the zone is not already the one the server will emit,
# e.g. left over from test_emit_router_timezone.
timedatectl('set-timezone', 'UTC')
start_networkd()
self.wait_online('veth-peer:carrier')
start_dnsmasq(
'--dhcp-option=option6:42,Europe/Berlin',
ra_mode='ra-stateless',
)
self.wait_online('veth99:routable', 'veth-peer:routable')
# The DHCPv6 REPLY for INFORMATION-REQUEST may be received after the
# link entered the configured state, and the timezone is applied
# through an asynchronous call to timedated. Wait for it.
tz = None
for _ in range(100):
r = run(*timedatectl_cmd, 'show', '--value', '--property', 'Timezone', env=env)
tz = r.stdout.rstrip()
if tz == 'Europe/Berlin':
break
time.sleep(0.2)
self.assertEqual(tz, 'Europe/Berlin')
def test_dhcp_client_ipv6_only(self):
copy_network_unit(
'25-veth.netdev',