dhcpv6: RFC4833 timezones
authorPaul Donald <[email protected]>
Wed, 22 Oct 2025 00:17:06 +0000 (02:17 +0200)
committerÁlvaro Fernández Rojas <[email protected]>
Wed, 22 Oct 2025 14:10:48 +0000 (16:10 +0200)
This implements RFC4833 - supplying timezone information to clients that
request them. Both forms are possible, when timezone is configured in
the uci system settings (the luci GUI saves both forms to the config).

e.g.
```
config system
option zonename 'America/Puerto Rico'
option timezone 'AST4'
```

There is also an odhcpd flag to disable their use, set in uci dhcp.

```
config odhcpd 'odhcpd'
option enable_tzdb '0'
```

Once enabled, the options, when requested, are sent:
NEW_POSIX_TIMEZONE 41 // 'AST4'
NEW_TZDB_TIMEZONE 42 // 'America/Puerto_Rico'

Wireshark disassemble of options sent to client:

```
...
Time Zone Database
    Option: Time Zone Database (42)
    Length: 19
    TZ-database: America/Puerto_Rico
Time Zone
    Option: Time Zone (41)
    Length: 4
    Time-zone: AST4
```

Signed-off-by: Paul Donald <[email protected]>
Link: https://github.com/openwrt/odhcpd/pull/284
Signed-off-by: Álvaro Fernández Rojas <[email protected]>
README.md
src/config.c
src/dhcpv6.c
src/dhcpv6.h
src/odhcpd.h

index cbea25f5f0e4864b9250abfb0791add52f3d017a..65787d77bd199bd02963e6ea1cc296692aaf3df8 100644 (file)
--- a/README.md
+++ b/README.md
@@ -68,6 +68,7 @@ and may also receive information from ubus
 | hostsfile    | string|       | DHCP/v6 hostfile |
 | loglevel     |integer| 6     | Syslog level priority (0-7) |
 | piofolder    |string |       | Folder to store IPv6 prefix information (to detect stale prefixes, see RFC9096, §3.5) |
+| enable_tz |bool | 1 | Toggle whether RFC4833 timezone information is sent to clients, if set in system  |
 
 
 ### Sections of type dhcp (configure DHCP / DHCPv6 / RA / NDP service)
@@ -138,6 +139,13 @@ and may also receive information from ubus
 | arch         |integer| no    | the arch code. `07` is EFI. If not present, this boot6 will be the default. |
 
 
+### System variables for Timezone options (uci system.system)
+| Option  | Type  |Required|Description |
+| :------------ | :---- | :---- | :---------- |
+| timezone   |string | no | e.g. `EST5EDT4,M3.2.0/02:00,M11.1.0/02:00` |
+| zonename    |string| no  | e.g. `Europe/Zurich` |
+
+
 ## ubus Interface
 
 odhcpd currently exposes the following methods under the `dhcp` object path:
index ddc4537000a1ef541351194268dedb3555855754..6a33b7ce937e37c9edcd184027da2cb5f748e3b8 100644 (file)
@@ -8,6 +8,7 @@
 #include <net/if.h>
 #include <string.h>
 #include <sys/stat.h>
+#include <ctype.h>
 
 #include <uci.h>
 #include <uci_blob.h>
@@ -33,6 +34,7 @@ struct vlist_tree leases = VLIST_TREE_INIT(leases, lease_cmp, lease_update, true
 AVL_TREE(interfaces, avl_strcmp, false, NULL);
 struct config config = {
        .legacy = false,
+       .enable_tz = true,
        .main_dhcpv4 = false,
        .dhcp_cb = NULL,
        .dhcp_statefile = NULL,
@@ -45,6 +47,14 @@ struct config config = {
        .log_syslog = true,
 };
 
+struct sys_conf sys_conf = {
+       .uci_cfgfile = "system",
+       .posix_tz = NULL, // "timezone"
+       .posix_tz_len = 0,
+       .tzdb_tz = NULL, // "zonename"
+       .tzdb_tz_len = 0,
+};
+
 #define START_DEFAULT  100
 #define LIMIT_DEFAULT  150
 
@@ -215,6 +225,7 @@ enum {
        ODHCPD_ATTR_LOGLEVEL,
        ODHCPD_ATTR_HOSTSFILE,
        ODHCPD_ATTR_PIOFOLDER,
+       ODHCPD_ATTR_ENABLE_TZ,
        ODHCPD_ATTR_MAX
 };
 
@@ -226,6 +237,7 @@ static const struct blobmsg_policy odhcpd_attrs[ODHCPD_ATTR_MAX] = {
        [ODHCPD_ATTR_LOGLEVEL] = { .name = "loglevel", .type = BLOBMSG_TYPE_INT32 },
        [ODHCPD_ATTR_HOSTSFILE] = { .name = "hostsfile", .type = BLOBMSG_TYPE_STRING },
        [ODHCPD_ATTR_PIOFOLDER] = { .name = "piofolder", .type = BLOBMSG_TYPE_STRING },
+       [ODHCPD_ATTR_ENABLE_TZ] = { .name = "enable_tz", .type = BLOBMSG_TYPE_BOOL },
 };
 
 const struct uci_blob_param_list odhcpd_attr_list = {
@@ -233,6 +245,22 @@ const struct uci_blob_param_list odhcpd_attr_list = {
        .params = odhcpd_attrs,
 };
 
+enum {
+       SYSTEM_ATTR_TIMEZONE,
+       SYSTEM_ATTR_ZONENAME,
+       SYSTEM_ATTR_MAX
+};
+
+static const struct blobmsg_policy system_attrs[SYSTEM_ATTR_MAX] = {
+       [SYSTEM_ATTR_TIMEZONE] = { .name = "timezone", .type = BLOBMSG_TYPE_STRING },
+       [SYSTEM_ATTR_ZONENAME] = { .name = "zonename", .type = BLOBMSG_TYPE_STRING },
+};
+
+const struct uci_blob_param_list system_attr_list = {
+       .n_params = SYSTEM_ATTR_MAX,
+       .params = system_attrs,
+};
+
 static const struct { const char *name; uint8_t flag; } ra_flags[] = {
        { .name = "managed-config", .flag = ND_RA_FLAG_MANAGED },
        { .name = "other-config", .flag = ND_RA_FLAG_OTHER },
@@ -432,6 +460,54 @@ static void set_config(struct uci_section *s)
                        notice("Log level set to %d\n", config.log_level);
                }
        }
+
+       if ((c = tb[ODHCPD_ATTR_ENABLE_TZ]))
+               config.enable_tz = blobmsg_get_bool(c);
+
+}
+
+static void sanitize_tz_string(const char *src, uint8_t **dst, size_t *dst_len)
+{
+       /* replace any spaces with '_' in tz strings. luci, where these strings
+       are normally set, (had a bug that) replaced underscores for spaces in the
+       names. */
+
+       if (!dst || !dst_len)
+               return;
+
+       free(*dst);
+       *dst = NULL;
+       *dst_len = 0;
+
+       if (!src || !*src)
+               return;
+
+       char *copy = strdup(src);
+       if (!copy)
+               return;
+
+       for (char *p = copy; *p; p++) {
+               if (isspace((unsigned char)*p))
+                       *p = '_';
+       }
+
+       *dst = (uint8_t *)copy;
+       *dst_len = strlen(copy);
+}
+
+static void set_timezone_info_from_uci(struct uci_section *s)
+{
+       struct blob_attr *tb[SYSTEM_ATTR_MAX], *c;
+
+       blob_buf_init(&b, 0);
+       uci_to_blob(&b, s, &system_attr_list);
+       blobmsg_parse(system_attrs, SYSTEM_ATTR_MAX, tb, blob_data(b.head), blob_len(b.head));
+
+       if ((c = tb[SYSTEM_ATTR_TIMEZONE]))
+               sanitize_tz_string(blobmsg_get_string(c), &sys_conf.posix_tz, &sys_conf.posix_tz_len);
+
+       if ((c = tb[SYSTEM_ATTR_ZONENAME]))
+               sanitize_tz_string(blobmsg_get_string(c), &sys_conf.tzdb_tz, &sys_conf.tzdb_tz_len);
 }
 
 static uint32_t parse_leasetime(struct blob_attr *c) {
@@ -2193,6 +2269,18 @@ void odhcpd_reload(void)
                ipv6_pxe_dump();
        }
 
+       struct uci_package *system = NULL;
+       if (!uci_load(uci, sys_conf.uci_cfgfile, &system) && config.enable_tz == true) {
+               struct uci_element *e;
+
+               /* 1. System settings */
+               uci_foreach_element(&system->sections, e) {
+                       struct uci_section *s = uci_to_section(e);
+                       if (!strcmp(s->type, "system"))
+                               set_timezone_info_from_uci(s);
+               }
+       }
+
        if (config.dhcp_statefile) {
                char *path = strdup(config.dhcp_statefile);
 
@@ -2287,6 +2375,7 @@ void odhcpd_reload(void)
        }
 
        uci_unload(uci, dhcp);
+       uci_unload(uci, system);
        uci_free_context(uci);
 }
 
index 8ce1bff9732898861ff49ded6e01725774295c3a..000370f0fb8f74a6fa08eae28ccd56c26693589e 100644 (file)
@@ -185,6 +185,10 @@ enum {
        IOV_DHCPV4O6_SERVER,
        IOV_DNR,
        IOV_BOOTFILE_URL,
+       IOV_POSIX_TZ,
+       IOV_POSIX_TZ_STR,
+       IOV_TZDB_TZ,
+       IOV_TZDB_TZ_STR,
        IOV_TOTAL
 };
 
@@ -430,6 +434,29 @@ static void handle_client_request(void *addr, void *data, size_t len,
                uint16_t len;
        } dhcpv6_sntp;
 
+       /* RFC 4833 - Timezones */
+       uint8_t *posix_ptr = sys_conf.posix_tz;
+       uint16_t posix_len = sys_conf.posix_tz_len;
+       /* RFC 4833 - OPTION_NEW_POSIX_TIMEZONE (41)
+        * e.g. EST5EDT4,M3.2.0/02:00,M11.1.0/02:00
+        * Variable-length opaque tz_string blob.
+        */
+       struct {
+               uint16_t type;
+               uint16_t len;
+       } posix_tz;
+
+       uint8_t *tzdb_ptr = sys_conf.tzdb_tz;
+       uint16_t tzdb_len = sys_conf.tzdb_tz_len;
+       /* RFC 4833 - OPTION_NEW_TZDB_TIMEZONE (42)
+        * e.g. Europe/Zurich
+        * Variable-length opaque tz_name blob.
+        */
+       struct {
+               uint16_t type;
+               uint16_t len;
+       } tzdb_tz;
+
        /* NTP */
        uint8_t *ntp_ptr = iface->dhcpv6_ntp;
        uint16_t ntp_len = iface->dhcpv6_ntp_len;
@@ -482,6 +509,16 @@ static void handle_client_request(void *addr, void *data, size_t len,
                        ntp.len = htons(ntp_len);
                        break;
 
+               case DHCPV6_OPT_NEW_POSIX_TIMEZONE:
+                       posix_tz.type = htons(DHCPV6_OPT_NEW_POSIX_TIMEZONE);
+                       posix_tz.len  = htons(posix_len);
+                       break;
+
+               case DHCPV6_OPT_NEW_TZDB_TIMEZONE:
+                       tzdb_tz.type = htons(DHCPV6_OPT_NEW_TZDB_TIMEZONE);
+                       tzdb_tz.len  = htons(tzdb_len);
+                       break;
+
                case DHCPV6_OPT_DNR:
                        for (size_t i = 0; i < iface->dnr_cnt; i++) {
                                struct dnr_options *dnr = &iface->dnr[i];
@@ -596,6 +633,10 @@ static void handle_client_request(void *addr, void *data, size_t len,
                [IOV_NTP_ADDR] = {ntp_ptr, (ntp_cnt) ? ntp_len : 0},
                [IOV_SNTP] = {&dhcpv6_sntp, (sntp_cnt) ? sizeof(dhcpv6_sntp) : 0},
                [IOV_SNTP_ADDR] = {sntp_addr_ptr, sntp_cnt * sizeof(*sntp_addr_ptr)},
+               [IOV_POSIX_TZ] = {&posix_tz, (posix_len) ? sizeof(posix_tz) : 0},
+               [IOV_POSIX_TZ_STR] = {posix_ptr, (posix_len) ? posix_len : 0 },
+               [IOV_TZDB_TZ] = {&tzdb_tz, (tzdb_len) ? sizeof(tzdb_tz) : 0},
+               [IOV_TZDB_TZ_STR] = {tzdb_ptr, (tzdb_len) ? tzdb_len : 0 },
                [IOV_DNR] = {dnrs, dnrs_len},
                [IOV_RELAY_MSG] = {NULL, 0},
                [IOV_DHCPV4O6_SERVER] = {&dhcpv4o6_server, 0},
@@ -751,6 +792,8 @@ static void handle_client_request(void *addr, void *data, size_t len,
                                      iov[IOV_CERID].iov_len + iov[IOV_DHCPV6_RAW].iov_len +
                                      iov[IOV_NTP].iov_len + iov[IOV_NTP_ADDR].iov_len +
                                      iov[IOV_SNTP].iov_len + iov[IOV_SNTP_ADDR].iov_len +
+                                     iov[IOV_POSIX_TZ].iov_len + iov[IOV_POSIX_TZ_STR].iov_len + 
+                                     iov[IOV_TZDB_TZ].iov_len + iov[IOV_TZDB_TZ_STR].iov_len +
                                      iov[IOV_DNR].iov_len + iov[IOV_BOOTFILE_URL].iov_len -
                                      (4 + opts_end - opts));
 
index 4220fe2b757cbe7cb59d9c1123c7ab28b8501007..5c3986cf5793fc5d64dad57d24c7193a324ec521 100644 (file)
 #define DHCPV6_OPT_SNTP_SERVERS 31
 #define DHCPV6_OPT_INFO_REFRESH 32
 #define DHCPV6_OPT_FQDN 39
+/* RFC 4833 */
+#define DHCPV6_OPT_NEW_POSIX_TIMEZONE 41
+#define DHCPV6_OPT_NEW_TZDB_TIMEZONE 42
+
 #define DHCPV6_OPT_NTP_SERVERS 56
 #define DHCPV6_OPT_BOOTFILE_URL 59
 #define DHCPV6_OPT_BOOTFILE_PARAM 60
index 02babc5e31a3df2cdf9b5cad91e24bf5fdad74da..693da99783035fbc5cdd8de40ec367adc4399c0b 100644 (file)
@@ -73,6 +73,7 @@ struct interface;
 struct nl_sock;
 extern struct vlist_tree leases;
 extern struct config config;
+extern struct sys_conf sys_conf;
 
 void __iflog(int lvl, const char *fmt, ...);
 #define debug(fmt, ...)     __iflog(LOG_DEBUG, fmt __VA_OPT__(, ) __VA_ARGS__)
@@ -184,6 +185,7 @@ enum odhcpd_assignment_flags {
 
 struct config {
        bool legacy;
+       bool enable_tz;
        bool main_dhcpv4;
        char *dhcp_cb;
        char *dhcp_statefile;
@@ -198,6 +200,14 @@ struct config {
        bool log_syslog;
 };
 
+struct sys_conf {
+       uint8_t *posix_tz;
+       size_t posix_tz_len;
+       char *uci_cfgfile;
+       uint8_t *tzdb_tz;
+       size_t tzdb_tz_len;
+};
+
 /* 2-byte type + 128-byte DUID, RFC8415, §11.1 */
 #define DUID_MAX_LEN 130
 #define DUID_HEXSTRLEN (DUID_MAX_LEN * 2 + 1)