/* dnsmasq is Copyright (c) 2000-2024 Simon Kelley This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; version 2 dated June, 1991, or (at your option) version 3 dated 29 June, 2007. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "dnsmasq.h" #ifdef HAVE_INOTIFY #include #include /* For MAXSYMLINKS */ /* the strategy is to set an inotify on the directories containing resolv files, for any files in the directory which are close-write or moved into the directory. When either of those happen, we look to see if the file involved is actually a resolv-file, and if so, call poll-resolv with the "force" argument, to ensure it's read. This adds one new error condition: the directories containing all specified resolv-files must exist at start-up, even if the actual files don't. */ static char *inotify_buffer; #define INOTIFY_SZ (sizeof(struct inotify_event) + NAME_MAX + 1) /* If path is a symbolic link, return the path it points to, made absolute if relative. If path doesn't exist or is not a symlink, return NULL. Return value is malloc'ed */ static char *my_readlink(char *path) { ssize_t rc, size = 64; char *buf; while (1) { buf = safe_malloc(size); rc = readlink(path, buf, (size_t)size); if (rc == -1) { /* Not link or doesn't exist. */ if (errno == EINVAL || errno == ENOENT) { free(buf); return NULL; } else die(_("cannot access path %s: %s"), path, EC_MISC); } else if (rc < size-1) { char *d; buf[rc] = 0; if (buf[0] != '/' && ((d = strrchr(path, '/')))) { /* Add path to relative link */ char *new_buf = safe_malloc((d - path) + strlen(buf) + 2); *(d+1) = 0; strcpy(new_buf, path); strcat(new_buf, buf); free(buf); buf = new_buf; } return buf; } /* Buffer too small, increase and retry */ size += 64; free(buf); } } void inotify_dnsmasq_init() { struct resolvc *res; inotify_buffer = safe_malloc(INOTIFY_SZ); daemon->inotifyfd = inotify_init1(IN_NONBLOCK | IN_CLOEXEC); if (daemon->inotifyfd == -1) die(_("failed to create inotify: %s"), NULL, EC_MISC); if (daemon->port == 0 || option_bool(OPT_NO_RESOLV)) return; for (res = daemon->resolv_files; res; res = res->next) { char *d, *new_path, *path = safe_malloc(strlen(res->name) + 1); int links = MAXSYMLINKS; strcpy(path, res->name); /* Follow symlinks until we reach a non-symlink, or a non-existent file. */ while ((new_path = my_readlink(path))) { if (links-- == 0) die(_("too many symlinks following %s"), res->name, EC_MISC); free(path); path = new_path; } res->wd = -1; if ((d = strrchr(path, '/'))) { *d = 0; /* make path just directory */ res->wd = inotify_add_watch(daemon->inotifyfd, path, IN_CLOSE_WRITE | IN_MOVED_TO); res->file = d+1; /* pointer to filename */ *d = '/'; if (res->wd == -1 && errno == ENOENT) die(_("directory %s for resolv-file is missing, cannot poll"), res->name, EC_MISC); } if (res->wd == -1) die(_("failed to create inotify for %s: %s"), res->name, EC_MISC); } } static struct hostsfile *dyndir_addhosts(struct dyndir *dd, char *path) { /* Check if this file is already known in dd->files */ struct hostsfile *ah = NULL; for(ah = dd->files; ah; ah = ah->next) if(ah && ah->fname && strcmp(path, ah->fname) == 0) return ah; /* Not known, create new hostsfile record for this dyndir */ struct hostsfile *newah = NULL; if(!(newah = whine_malloc(sizeof(struct hostsfile)))) return NULL; /* Add this file to the tip of the linked list */ newah->next = dd->files; dd->files = newah; /* Copy flags, set index and the full file path */ newah->flags = dd->flags; newah->index = daemon->host_index++; newah->fname = path; return newah; } /* initialisation for dynamic-dir. Set inotify watch for each directory, and read pre-existing files */ void set_dynamic_inotify(int flag, int total_size, struct crec **rhash, int revhashsz) { struct dyndir *dd; for (dd = daemon->dynamic_dirs; dd; dd = dd->next) { DIR *dir_stream = NULL; struct dirent *ent; struct stat buf; if (!(dd->flags & flag)) continue; if (stat(dd->dname, &buf) == -1) { my_syslog(LOG_ERR, _("bad dynamic directory %s: %s"), dd->dname, strerror(errno)); continue; } if (!(S_ISDIR(buf.st_mode))) { my_syslog(LOG_ERR, _("bad dynamic directory %s: %s"), dd->dname, _("not a directory")); continue; } if (!(dd->flags & AH_WD_DONE)) { dd->wd = inotify_add_watch(daemon->inotifyfd, dd->dname, IN_CLOSE_WRITE | IN_MOVED_TO | IN_DELETE); dd->flags |= AH_WD_DONE; } /* Read contents of dir _after_ calling add_watch, in the hope of avoiding a race which misses files being added as we start */ if (dd->wd == -1 || !(dir_stream = opendir(dd->dname))) { my_syslog(LOG_ERR, _("failed to create inotify for %s: %s"), dd->dname, strerror(errno)); continue; } while ((ent = readdir(dir_stream))) { size_t lendir = strlen(dd->dname); size_t lenfile = strlen(ent->d_name); char *path; /* ignore emacs backups and dotfiles */ if (lenfile == 0 || ent->d_name[lenfile - 1] == '~' || (ent->d_name[0] == '#' && ent->d_name[lenfile - 1] == '#') || ent->d_name[0] == '.') continue; if ((path = whine_malloc(lendir + lenfile + 2))) { struct hostsfile *ah; strcpy(path, dd->dname); strcat(path, "/"); strcat(path, ent->d_name); if (!(ah = dyndir_addhosts(dd, path))) { free(path); continue; } /* ignore non-regular files */ if (stat(path, &buf) != -1 && S_ISREG(buf.st_mode)) { if (dd->flags & AH_HOSTS) total_size = read_hostsfile(path, ah->index, total_size, rhash, revhashsz); #ifdef HAVE_DHCP else if (dd->flags & (AH_DHCP_HST | AH_DHCP_OPT)) option_read_dynfile(path, dd->flags); #endif } } } closedir(dir_stream); } } int inotify_check(time_t now) { int hit = 0; struct dyndir *dd; while (1) { int rc; char *p; struct resolvc *res; struct inotify_event *in; while ((rc = read(daemon->inotifyfd, inotify_buffer, INOTIFY_SZ)) == -1 && errno == EINTR); if (rc <= 0) break; for (p = inotify_buffer; rc - (p - inotify_buffer) >= (int)sizeof(struct inotify_event); p += sizeof(struct inotify_event) + in->len) { size_t namelen; in = (struct inotify_event*)p; /* ignore emacs backups and dotfiles */ if (in->len == 0 || (namelen = strlen(in->name)) == 0 || in->name[namelen - 1] == '~' || (in->name[0] == '#' && in->name[namelen - 1] == '#') || in->name[0] == '.') continue; for (res = daemon->resolv_files; res; res = res->next) if (res->wd == in->wd && strcmp(res->file, in->name) == 0) hit = 1; for (dd = daemon->dynamic_dirs; dd; dd = dd->next) if (dd->wd == in->wd) { size_t lendir = strlen(dd->dname); char *path; if ((path = whine_malloc(lendir + in->len + 2))) { struct hostsfile *ah = NULL; strcpy(path, dd->dname); strcat(path, "/"); strcat(path, in->name); /* Is this is a deletion event? */ if (in->mask & IN_DELETE) my_syslog(LOG_INFO, _("inotify: %s removed"), path); else my_syslog(LOG_INFO, _("inotify: %s new or modified"), path); if (dd->flags & AH_HOSTS) { if ((ah = dyndir_addhosts(dd, path))) { const unsigned int removed = cache_remove_uid(ah->index); if (removed > 0) my_syslog(LOG_INFO, _("inotify: flushed %u names read from %s"), removed, path); /* (Re-)load hostsfile only if this event isn't triggered by deletion */ if (!(in->mask & IN_DELETE)) read_hostsfile(path, ah->index, 0, NULL, 0); #ifdef HAVE_DHCP if (daemon->dhcp || daemon->doing_dhcp6) { /* Propagate the consequences of loading a new dhcp-host */ dhcp_update_configs(daemon->dhcp_conf); lease_update_from_configs(); lease_update_file(now); lease_update_dns(1); } #endif } } #ifdef HAVE_DHCP else if (dd->flags & AH_DHCP_HST) { if (option_read_dynfile(path, AH_DHCP_HST)) { /* Propagate the consequences of loading a new dhcp-host */ dhcp_update_configs(daemon->dhcp_conf); lease_update_from_configs(); lease_update_file(now); lease_update_dns(1); } } else if (dd->flags & AH_DHCP_OPT) option_read_dynfile(path, AH_DHCP_OPT); #endif if (!ah) free(path); } } } } return hit; } #endif /* INOTIFY */