| /* MIT License |
| * |
| * Copyright (c) 2024 Brad House |
| * |
| * Permission is hereby granted, free of charge, to any person obtaining a copy |
| * of this software and associated documentation files (the "Software"), to deal |
| * in the Software without restriction, including without limitation the rights |
| * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
| * copies of the Software, and to permit persons to whom the Software is |
| * furnished to do so, subject to the following conditions: |
| * |
| * The above copyright notice and this permission notice (including the next |
| * paragraph) shall be included in all copies or substantial portions of the |
| * Software. |
| * |
| * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
| * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
| * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
| * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
| * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
| * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
| * SOFTWARE. |
| * |
| * SPDX-License-Identifier: MIT |
| */ |
| #include "ares_setup.h" |
| #include "ares.h" |
| #include "ares_private.h" |
| #include "ares_event.h" |
| |
| static void ares_event_configchg_reload(ares_event_thread_t *e) |
| { |
| ares_reinit(e->channel); |
| } |
| |
| #ifdef __linux__ |
| |
| # include <sys/inotify.h> |
| |
| struct ares_event_configchg { |
| int inotify_fd; |
| ares_event_thread_t *e; |
| }; |
| |
| void ares_event_configchg_destroy(ares_event_configchg_t *configchg) |
| { |
| if (configchg == NULL) { |
| return; |
| } |
| |
| /* Tell event system to stop monitoring for changes. This will cause the |
| * cleanup to be called */ |
| ares_event_update(NULL, configchg->e, ARES_EVENT_FLAG_NONE, NULL, |
| configchg->inotify_fd, NULL, NULL, NULL); |
| } |
| |
| static void ares_event_configchg_free(void *data) |
| { |
| ares_event_configchg_t *configchg = data; |
| if (configchg == NULL) { |
| return; |
| } |
| |
| if (configchg->inotify_fd >= 0) { |
| close(configchg->inotify_fd); |
| configchg->inotify_fd = -1; |
| } |
| |
| ares_free(configchg); |
| } |
| |
| static void ares_event_configchg_cb(ares_event_thread_t *e, ares_socket_t fd, |
| void *data, ares_event_flags_t flags) |
| { |
| ares_event_configchg_t *configchg = data; |
| |
| /* Some systems cannot read integer variables if they are not |
| * properly aligned. On other systems, incorrect alignment may |
| * decrease performance. Hence, the buffer used for reading from |
| * the inotify file descriptor should have the same alignment as |
| * struct inotify_event. */ |
| unsigned char buf[4096] |
| __attribute__((aligned(__alignof__(struct inotify_event)))); |
| const struct inotify_event *event; |
| ssize_t len; |
| ares_bool_t triggered = ARES_FALSE; |
| |
| (void)fd; |
| (void)flags; |
| |
| while (1) { |
| const unsigned char *ptr; |
| |
| len = read(configchg->inotify_fd, buf, sizeof(buf)); |
| if (len <= 0) { |
| break; |
| } |
| |
| /* Loop over all events in the buffer. Says kernel will check the buffer |
| * size provided, so I assume it won't ever return partial events. */ |
| for (ptr = buf; ptr < buf + len; |
| ptr += sizeof(struct inotify_event) + event->len) { |
| event = (const struct inotify_event *)ptr; |
| |
| if (event->len == 0 || ares_strlen(event->name) == 0) { |
| continue; |
| } |
| |
| if (strcasecmp(event->name, "resolv.conf") == 0 || |
| strcasecmp(event->name, "nsswitch.conf") == 0) { |
| triggered = ARES_TRUE; |
| } |
| } |
| } |
| |
| /* Only process after all events are read. No need to process more often as |
| * we don't want to reload the config back to back */ |
| if (triggered) { |
| ares_event_configchg_reload(e); |
| } |
| } |
| |
| ares_status_t ares_event_configchg_init(ares_event_configchg_t **configchg, |
| ares_event_thread_t *e) |
| { |
| ares_status_t status = ARES_SUCCESS; |
| ares_event_configchg_t *c; |
| |
| (void)e; |
| |
| /* Not used by this implementation */ |
| *configchg = NULL; |
| |
| c = ares_malloc_zero(sizeof(*c)); |
| if (c == NULL) { |
| return ARES_ENOMEM; |
| } |
| |
| c->e = e; |
| c->inotify_fd = inotify_init1(IN_NONBLOCK | IN_CLOEXEC); |
| if (c->inotify_fd == -1) { |
| status = ARES_ESERVFAIL; |
| goto done; |
| } |
| |
| /* We need to monitor /etc/resolv.conf, /etc/nsswitch.conf */ |
| if (inotify_add_watch(c->inotify_fd, "/etc", |
| IN_CREATE | IN_MODIFY | IN_MOVED_TO | IN_ONLYDIR) == |
| -1) { |
| status = ARES_ESERVFAIL; |
| goto done; |
| } |
| |
| status = |
| ares_event_update(NULL, e, ARES_EVENT_FLAG_READ, ares_event_configchg_cb, |
| c->inotify_fd, c, ares_event_configchg_free, NULL); |
| |
| done: |
| if (status != ARES_SUCCESS) { |
| ares_event_configchg_free(c); |
| } |
| return status; |
| } |
| |
| #elif defined(_WIN32) |
| |
| # include <winsock2.h> |
| # include <iphlpapi.h> |
| # include <stdio.h> |
| # include <windows.h> |
| |
| struct ares_event_configchg { |
| HANDLE ifchg_hnd; |
| ares_event_thread_t *e; |
| }; |
| |
| void ares_event_configchg_destroy(ares_event_configchg_t *configchg) |
| { |
| # ifdef __WATCOMC__ |
| /* Not supported */ |
| # else |
| if (configchg == NULL) { |
| return; |
| } |
| |
| if (configchg->ifchg_hnd != NULL) { |
| CancelMibChangeNotify2(configchg->ifchg_hnd); |
| configchg->ifchg_hnd = NULL; |
| } |
| |
| ares_free(configchg); |
| # endif |
| } |
| |
| # ifndef __WATCOMC__ |
| static void ares_event_configchg_cb(PVOID CallerContext, |
| PMIB_IPINTERFACE_ROW Row, |
| MIB_NOTIFICATION_TYPE NotificationType) |
| { |
| ares_event_configchg_t *configchg = CallerContext; |
| (void)Row; |
| (void)NotificationType; |
| ares_event_configchg_reload(configchg->e); |
| } |
| # endif |
| |
| |
| ares_status_t ares_event_configchg_init(ares_event_configchg_t **configchg, |
| ares_event_thread_t *e) |
| { |
| # ifdef __WATCOMC__ |
| return ARES_ENOTIMP; |
| # else |
| ares_status_t status = ARES_SUCCESS; |
| |
| *configchg = ares_malloc_zero(sizeof(**configchg)); |
| if (*configchg == NULL) { |
| return ARES_ENOMEM; |
| } |
| |
| (*configchg)->e = e; |
| |
| /* NOTE: If a user goes into the control panel and changes the network |
| * adapter DNS addresses manually, this will NOT trigger a notification. |
| * We've also tried listening on NotifyUnicastIpAddressChange(), but |
| * that didn't get triggered either. |
| */ |
| |
| if (NotifyIpInterfaceChange( |
| AF_UNSPEC, (PIPINTERFACE_CHANGE_CALLBACK)ares_event_configchg_cb, |
| *configchg, FALSE, &(*configchg)->ifchg_hnd) != NO_ERROR) { |
| status = ARES_ESERVFAIL; |
| goto done; |
| } |
| |
| done: |
| if (status != ARES_SUCCESS) { |
| ares_event_configchg_destroy(*configchg); |
| *configchg = NULL; |
| } |
| |
| return status; |
| # endif |
| } |
| |
| #elif defined(__APPLE__) |
| |
| # include <sys/types.h> |
| # include <unistd.h> |
| # include <notify.h> |
| # include <dlfcn.h> |
| |
| struct ares_event_configchg { |
| int fd; |
| int token; |
| }; |
| |
| void ares_event_configchg_destroy(ares_event_configchg_t *configchg) |
| { |
| (void)configchg; |
| |
| /* Cleanup happens automatically */ |
| } |
| |
| static void ares_event_configchg_free(void *data) |
| { |
| ares_event_configchg_t *configchg = data; |
| if (configchg == NULL) { |
| return; |
| } |
| |
| if (configchg->fd >= 0) { |
| notify_cancel(configchg->token); |
| /* automatically closes fd */ |
| configchg->fd = -1; |
| } |
| |
| ares_free(configchg); |
| } |
| |
| static void ares_event_configchg_cb(ares_event_thread_t *e, ares_socket_t fd, |
| void *data, ares_event_flags_t flags) |
| { |
| ares_event_configchg_t *configchg = data; |
| ares_bool_t triggered = ARES_FALSE; |
| |
| (void)fd; |
| (void)flags; |
| |
| while (1) { |
| int t = 0; |
| ssize_t len; |
| |
| len = read(configchg->fd, &t, sizeof(t)); |
| |
| if (len < (ssize_t)sizeof(t)) { |
| break; |
| } |
| |
| /* Token is read in network byte order (yeah, docs don't mention this) */ |
| t = (int)ntohl(t); |
| |
| if (t != configchg->token) { |
| continue; |
| } |
| |
| triggered = ARES_TRUE; |
| } |
| |
| /* Only process after all events are read. No need to process more often as |
| * we don't want to reload the config back to back */ |
| if (triggered) { |
| ares_event_configchg_reload(e); |
| } |
| } |
| |
| ares_status_t ares_event_configchg_init(ares_event_configchg_t **configchg, |
| ares_event_thread_t *e) |
| { |
| ares_status_t status = ARES_SUCCESS; |
| void *handle = NULL; |
| const char *(*pdns_configuration_notify_key)(void) = NULL; |
| const char *notify_key = NULL; |
| int flags; |
| |
| *configchg = ares_malloc_zero(sizeof(**configchg)); |
| if (*configchg == NULL) { |
| return ARES_ENOMEM; |
| } |
| |
| /* Load symbol as it isn't normally public */ |
| handle = dlopen("/usr/lib/libSystem.dylib", RTLD_LAZY | RTLD_NOLOAD); |
| if (handle == NULL) { |
| status = ARES_ESERVFAIL; |
| goto done; |
| } |
| |
| pdns_configuration_notify_key = dlsym(handle, "dns_configuration_notify_key"); |
| |
| if (pdns_configuration_notify_key == NULL) { |
| status = ARES_ESERVFAIL; |
| goto done; |
| } |
| |
| notify_key = pdns_configuration_notify_key(); |
| if (notify_key == NULL) { |
| status = ARES_ESERVFAIL; |
| goto done; |
| } |
| |
| if (notify_register_file_descriptor(notify_key, &(*configchg)->fd, 0, |
| &(*configchg)->token) != |
| NOTIFY_STATUS_OK) { |
| status = ARES_ESERVFAIL; |
| goto done; |
| } |
| |
| /* Set file descriptor to non-blocking */ |
| flags = fcntl((*configchg)->fd, F_GETFL, 0); |
| fcntl((*configchg)->fd, F_SETFL, flags | O_NONBLOCK); |
| |
| /* Register file descriptor with event subsystem */ |
| status = ares_event_update(NULL, e, ARES_EVENT_FLAG_READ, |
| ares_event_configchg_cb, (*configchg)->fd, |
| *configchg, ares_event_configchg_free, NULL); |
| |
| done: |
| if (status != ARES_SUCCESS) { |
| ares_event_configchg_free(*configchg); |
| *configchg = NULL; |
| } |
| |
| if (handle) { |
| dlclose(handle); |
| } |
| |
| return status; |
| } |
| |
| #elif defined(HAVE_STAT) |
| # ifdef HAVE_SYS_TYPES_H |
| # include <sys/types.h> |
| # endif |
| # ifdef HAVE_SYS_STAT_H |
| # include <sys/stat.h> |
| # endif |
| |
| typedef struct { |
| size_t size; |
| time_t mtime; |
| } fileinfo_t; |
| |
| struct ares_event_configchg { |
| ares_bool_t isup; |
| ares__thread_t *thread; |
| ares__htable_strvp_t *filestat; |
| ares__thread_mutex_t *lock; |
| ares__thread_cond_t *wake; |
| const char *resolvconf_path; |
| ares_event_thread_t *e; |
| }; |
| |
| static ares_status_t config_change_check(ares__htable_strvp_t *filestat, |
| const char *resolvconf_path) |
| { |
| size_t i; |
| const char *configfiles[] = { resolvconf_path, "/etc/nsswitch.conf", |
| "/etc/netsvc.conf", "/etc/svc.conf", NULL }; |
| ares_bool_t changed = ARES_FALSE; |
| |
| for (i = 0; configfiles[i] != NULL; i++) { |
| fileinfo_t *fi = ares__htable_strvp_get_direct(filestat, configfiles[i]); |
| struct stat st; |
| |
| if (stat(configfiles[i], &st) == 0) { |
| if (fi == NULL) { |
| fi = ares_malloc_zero(sizeof(*fi)); |
| if (fi == NULL) { |
| return ARES_ENOMEM; |
| } |
| if (!ares__htable_strvp_insert(filestat, configfiles[i], fi)) { |
| ares_free(fi); |
| return ARES_ENOMEM; |
| } |
| } |
| if (fi->size != (size_t)st.st_size || fi->mtime != (time_t)st.st_mtime) { |
| changed = ARES_TRUE; |
| } |
| fi->size = (size_t)st.st_size; |
| fi->mtime = (time_t)st.st_mtime; |
| } else if (fi != NULL) { |
| /* File no longer exists, remove */ |
| ares__htable_strvp_remove(filestat, configfiles[i]); |
| changed = ARES_TRUE; |
| } |
| } |
| |
| if (changed) { |
| return ARES_SUCCESS; |
| } |
| return ARES_ENOTFOUND; |
| } |
| |
| static void *ares_event_configchg_thread(void *arg) |
| { |
| ares_event_configchg_t *c = arg; |
| |
| ares__thread_mutex_lock(c->lock); |
| while (c->isup) { |
| ares_status_t status; |
| |
| if (ares__thread_cond_timedwait(c->wake, c->lock, 30000) != ARES_ETIMEOUT) { |
| continue; |
| } |
| |
| /* make sure status didn't change even though we got a timeout */ |
| if (!c->isup) { |
| break; |
| } |
| |
| status = config_change_check(c->filestat, c->resolvconf_path); |
| if (status == ARES_SUCCESS) { |
| ares_event_configchg_reload(c->e); |
| } |
| } |
| |
| ares__thread_mutex_unlock(c->lock); |
| return NULL; |
| } |
| |
| ares_status_t ares_event_configchg_init(ares_event_configchg_t **configchg, |
| ares_event_thread_t *e) |
| { |
| ares_status_t status = ARES_SUCCESS; |
| ares_event_configchg_t *c = NULL; |
| |
| *configchg = NULL; |
| |
| c = ares_malloc_zero(sizeof(*c)); |
| if (c == NULL) { |
| status = ARES_ENOMEM; |
| goto done; |
| } |
| |
| c->e = e; |
| |
| c->filestat = ares__htable_strvp_create(ares_free); |
| if (c->filestat == NULL) { |
| status = ARES_ENOMEM; |
| goto done; |
| } |
| |
| c->wake = ares__thread_cond_create(); |
| if (c->wake == NULL) { |
| status = ARES_ENOMEM; |
| goto done; |
| } |
| |
| c->resolvconf_path = c->e->channel->resolvconf_path; |
| if (c->resolvconf_path == NULL) { |
| c->resolvconf_path = PATH_RESOLV_CONF; |
| } |
| |
| status = config_change_check(c->filestat, c->resolvconf_path); |
| if (status == ARES_ENOMEM) { |
| goto done; |
| } |
| |
| c->isup = ARES_TRUE; |
| status = ares__thread_create(&c->thread, ares_event_configchg_thread, c); |
| |
| done: |
| if (status != ARES_SUCCESS) { |
| ares_event_configchg_destroy(c); |
| } else { |
| *configchg = c; |
| } |
| return status; |
| } |
| |
| void ares_event_configchg_destroy(ares_event_configchg_t *configchg) |
| { |
| if (configchg == NULL) { |
| return; |
| } |
| |
| if (configchg->lock) { |
| ares__thread_mutex_lock(configchg->lock); |
| } |
| |
| configchg->isup = ARES_FALSE; |
| if (configchg->wake) { |
| ares__thread_cond_signal(configchg->wake); |
| } |
| |
| if (configchg->lock) { |
| ares__thread_mutex_unlock(configchg->lock); |
| } |
| |
| if (configchg->thread) { |
| void *rv = NULL; |
| ares__thread_join(configchg->thread, &rv); |
| } |
| |
| ares__thread_mutex_destroy(configchg->lock); |
| ares__thread_cond_destroy(configchg->wake); |
| ares__htable_strvp_destroy(configchg->filestat); |
| ares_free(configchg); |
| } |
| |
| #else |
| |
| ares_status_t ares_event_configchg_init(ares_event_configchg_t **configchg, |
| ares_event_thread_t *e) |
| { |
| /* No ability */ |
| return ARES_ENOTIMP; |
| } |
| |
| void ares_event_configchg_destroy(ares_event_configchg_t *configchg) |
| { |
| /* No-op */ |
| } |
| |
| #endif |