statefiles: support per-interface hosts files
authorDavid Härdeman <[email protected]>
Sat, 8 Nov 2025 18:52:44 +0000 (19:52 +0100)
committerÁlvaro Fernández Rojas <[email protected]>
Tue, 11 Nov 2025 07:31:25 +0000 (08:31 +0100)
With this change, the "hostsfile" option is changed to a "hostsdir"
option. If set, per-interface hosts files will be written to the
directory (this requires some changes to the UCI defaults in the OpenWrt
repo). This prevents the hostnames from one interface "leaking" into
other interfaces, which might not be desirable e.g. when running several
instances of dnsmasq.

dnsmasq already supports reading several hosts files from a directory
(via the "--hostsdir=" option), so some changes to dnsmasq's init script
will also be necessary (depending on whether the user wants dnsmasq to
support resolution of all hosts or just the host on a given interface).

Making hosts files (rather than odhcpd's state file) the primary
interface for dnsmasq to be made aware of active leases/hostnames is
also an important first step towards making the statefile properly
private (the other important consumer is LuCI, via its rpcd plugin),
which is necessary if we want to support persisting leases across
restarts of odhcpd (or even reboots, if writing to flash is deemed
acceptable) in the future (since I want to extend the state file to
capture the full state of leases so that they can be properly
reconstructed).

In addition, the use of "--hostsdir=" in dnsmasq would make the
"leasetrigger" unnecessary, since it just restarts dnsmasq to make it
aware of the changes to the hostsfile. That still leaves the case when
dnsmasq uses multiple instances and only wants to read a specific
hostsfile (which, AFAIK, doesn't support automatic detection of when
that specific file changes).

Closes: https://github.com/openwrt/odhcpd/issues/237
Signed-off-by: David Härdeman <[email protected]>
Link: https://github.com/openwrt/odhcpd/pull/302
Signed-off-by: Álvaro Fernández Rojas <[email protected]>
README.md
src/config.c
src/odhcpd.h
src/statefiles.c
src/statefiles.h

index c9ac2ec3c08df655d99072788fe8435edabf61a3..b81055507d7c328521fb174aecd6eca26b946dd1 100644 (file)
--- a/README.md
+++ b/README.md
@@ -26,7 +26,7 @@ prefix delegation and can be used to relay RA, DHCPv6 and NDP between routed
      * stateless and stateful address assignment
      * prefix delegation support
      * dynamic reconfiguration in case prefixes change
-     * hostname detection and hosts-file creation
+     * hostname detection and hosts-files creation
 
    * relay: mostly standards-compliant DHCPv6-relay
      * support for rewriting announced DNS-server addresses
@@ -64,7 +64,7 @@ and may also receive information from ubus
 | maindhcp     | bool  | 0     | Use odhcpd as the main DHCPv4 service |
 | leasefile    | string|       | DHCP/v6 lease/hostfile |
 | leasetrigger | string|       | Lease trigger script |
-| hostsfile    | string|       | DHCP/v6 hostfile |
+| hostsdir     | string|       | DHCP/v6 hostfile directory (one file per interface will be created) |
 | 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  |
index 0cb6d55f4ab60c9e4b8dbcd0ac403ec2beefc40e..a1979a79222f165a2420e881fa5956d6edcdcbe1 100644 (file)
@@ -40,7 +40,7 @@ struct config config = {
        .dhcp_cb = NULL,
        .dhcp_statefile = NULL,
        .dhcp_statedir_fd = -1,
-       .dhcp_hostsfile = NULL,
+       .dhcp_hostsdir = NULL,
        .dhcp_hostsdir_fd = -1,
        .ra_piofolder = NULL,
        .ra_piofolder_fd = -1,
@@ -213,7 +213,7 @@ enum {
        ODHCPD_ATTR_LEASEFILE,
        ODHCPD_ATTR_LEASETRIGGER,
        ODHCPD_ATTR_LOGLEVEL,
-       ODHCPD_ATTR_HOSTSFILE,
+       ODHCPD_ATTR_HOSTSDIR,
        ODHCPD_ATTR_PIOFOLDER,
        ODHCPD_ATTR_ENABLE_TZ,
        ODHCPD_ATTR_MAX
@@ -224,7 +224,7 @@ static const struct blobmsg_policy odhcpd_attrs[ODHCPD_ATTR_MAX] = {
        [ODHCPD_ATTR_LEASEFILE] = { .name = "leasefile", .type = BLOBMSG_TYPE_STRING },
        [ODHCPD_ATTR_LEASETRIGGER] = { .name = "leasetrigger", .type = BLOBMSG_TYPE_STRING },
        [ODHCPD_ATTR_LOGLEVEL] = { .name = "loglevel", .type = BLOBMSG_TYPE_INT32 },
-       [ODHCPD_ATTR_HOSTSFILE] = { .name = "hostsfile", .type = BLOBMSG_TYPE_STRING },
+       [ODHCPD_ATTR_HOSTSDIR] = { .name = "hostsdir", .type = BLOBMSG_TYPE_STRING },
        [ODHCPD_ATTR_PIOFOLDER] = { .name = "piofolder", .type = BLOBMSG_TYPE_STRING },
        [ODHCPD_ATTR_ENABLE_TZ] = { .name = "enable_tz", .type = BLOBMSG_TYPE_BOOL },
 };
@@ -456,9 +456,9 @@ static void set_config(struct uci_section *s)
                config.dhcp_statefile = strdup(blobmsg_get_string(c));
        }
 
-       if ((c = tb[ODHCPD_ATTR_HOSTSFILE])) {
-               free(config.dhcp_hostsfile);
-               config.dhcp_hostsfile = strdup(blobmsg_get_string(c));
+       if ((c = tb[ODHCPD_ATTR_HOSTSDIR])) {
+               free(config.dhcp_hostsdir);
+               config.dhcp_hostsdir = strdup(blobmsg_get_string(c));
        }
 
        if ((c = tb[ODHCPD_ATTR_PIOFOLDER])) {
@@ -2321,11 +2321,9 @@ void odhcpd_reload(void)
                        error("Unable to open statedir: '%s': %m", dir);
        }
 
-       if (config.dhcp_hostsfile) {
-               char *dir = dirname(strdupa(config.dhcp_hostsfile));
-               char *file = basename(config.dhcp_hostsfile);
+       if (config.dhcp_hostsdir) {
+               char *dir = strdupa(config.dhcp_hostsdir);
 
-               memmove(config.dhcp_hostsfile, file, strlen(file) + 1);
                mkdir_p(dir, 0755);
 
                close(config.dhcp_hostsdir_fd);
index 5eaf4ce97fc45d01295513dba940674d2c2cc4e4..354c52934dffa84d5e01879514a58392d29e132c 100644 (file)
@@ -200,7 +200,7 @@ struct config {
 
        char *dhcp_statefile;
        int dhcp_statedir_fd;
-       char *dhcp_hostsfile;
+       char *dhcp_hostsdir;
        int dhcp_hostsdir_fd;
 
        char *ra_piofolder;
index b362a1ae0c5d9b116dd11f70472ede135cb2a843..c5f2b57eb15735444261617df0aa1d0363e6b79d 100644 (file)
@@ -60,7 +60,7 @@ static bool statefiles_write_host6(struct write_ctxt *ctxt, struct dhcpv6_lease
        if (!lease->hostname || lease->flags & OAF_BROKEN_HOSTNAME || !(lease->flags & OAF_DHCPV6_NA))
                return false;
 
-       if (!ctxt->fp) {
+       if (ctxt->fp) {
                inet_ntop(AF_INET6, addr, ipbuf, sizeof(ipbuf));
                statefiles_write_host(ipbuf, lease->hostname, ctxt);
        }
@@ -84,7 +84,7 @@ static bool statefiles_write_host4(struct write_ctxt *ctxt, struct dhcpv4_lease
        if (!lease->hostname || lease->flags & OAF_BROKEN_HOSTNAME)
                return false;
 
-       if (!ctxt->fp) {
+       if (ctxt->fp) {
                inet_ntop(AF_INET, &addr, ipbuf, sizeof(ipbuf));
                statefiles_write_host(ipbuf, lease->hostname, ctxt);
        }
@@ -95,32 +95,32 @@ static bool statefiles_write_host4(struct write_ctxt *ctxt, struct dhcpv4_lease
 static void statefiles_write_hosts(time_t now)
 {
        struct write_ctxt ctxt;
-       size_t tmp_hostsfile_strlen;
-       char *tmp_hostsfile;
+       const char *tmp_hostsfile = ".odhcpd.hosts";
        int fd;
 
-       if (config.dhcp_hostsdir_fd < 0 || !config.dhcp_hostsfile)
+       if (config.dhcp_hostsdir_fd < 0)
                return;
 
-       tmp_hostsfile_strlen = strlen(config.dhcp_hostsfile) + 2;
-       tmp_hostsfile = alloca(tmp_hostsfile_strlen);
-       sprintf(tmp_hostsfile, ".%s", config.dhcp_hostsfile);
+       avl_for_each_element(&interfaces, ctxt.iface, avl) {
+               char *hostsfile;
 
-       fd = openat(config.dhcp_hostsdir_fd, tmp_hostsfile, O_CREAT | O_WRONLY | O_CLOEXEC, 0644);
-       if (fd < 0)
-               goto err;
+               hostsfile = alloca(strlen(ODHCPD_HOSTS_FILE_PREFIX) + strlen(ctxt.iface->name) + 1);
+               sprintf(hostsfile, "%s.%s", ODHCPD_HOSTS_FILE_PREFIX, ctxt.iface->name);
 
-       if (lockf(fd, F_LOCK, 0) < 0)
-               goto err;
+               fd = openat(config.dhcp_hostsdir_fd, tmp_hostsfile, O_CREAT | O_WRONLY | O_CLOEXEC, 0644);
+               if (fd < 0)
+                       goto err;
 
-       if (ftruncate(fd, 0) < 0)
-               goto err;
+               if (lockf(fd, F_LOCK, 0) < 0)
+                       goto err;
+
+               if (ftruncate(fd, 0) < 0)
+                       goto err;
 
-       ctxt.fp = fdopen(fd, "w");
-       if (!ctxt.fp)
-               goto err;
+               ctxt.fp = fdopen(fd, "w");
+               if (!ctxt.fp)
+                       goto err;
 
-       avl_for_each_element(&interfaces, ctxt.iface, avl) {
                if (ctxt.iface->dhcpv6 == MODE_SERVER) {
                        struct dhcpv6_lease *lease;
 
@@ -149,11 +149,12 @@ static void statefiles_write_hosts(time_t now)
                                statefiles_write_host4(&ctxt, lease);
                        }
                }
+
+               fclose(ctxt.fp);
+               renameat(config.dhcp_hostsdir_fd, tmp_hostsfile,
+                        config.dhcp_hostsdir_fd, hostsfile);
        }
 
-       fclose(ctxt.fp);
-       renameat(config.dhcp_hostsdir_fd, tmp_hostsfile,
-                config.dhcp_hostsdir_fd, config.dhcp_hostsfile);
        return;
 
 err:
index 9acd9ab4d918d430ea5944430c50ac43dc9bf323..dec1aa1382f5326c21d1e29bac130df2e2a36541 100644 (file)
@@ -7,6 +7,8 @@
 #ifndef _STATEFILES_H_
 #define _STATEFILES_H_
 
+#define ODHCPD_HOSTS_FILE_PREFIX "odhcpd.hosts"
+
 bool statefiles_write(void);
 
 #endif /* _STATEFILES_H_ */