// SPDX-License-Identifier: GPL-2.0+ /* * Copyright (C) 2010 Dan Williams */ #include "nm-default.h" #include "nm-dns-dnsmasq.h" #include #include #include #include #include #include #include #include "nm-glib-aux/nm-dbus-aux.h" #include "nm-core-internal.h" #include "platform/nm-platform.h" #include "nm-utils.h" #include "nm-ip4-config.h" #include "nm-ip6-config.h" #include "nm-dbus-manager.h" #include "NetworkManagerUtils.h" #define PIDFILE NMRUNDIR "/dnsmasq.pid" #define CONFDIR NMCONFDIR "/dnsmasq.d" #define DNSMASQ_DBUS_SERVICE "org.freedesktop.NetworkManager.dnsmasq" #define DNSMASQ_DBUS_PATH "/uk/org/thekelleys/dnsmasq" #define RATELIMIT_INTERVAL_MSEC 30000 #define RATELIMIT_BURST 5 #define _NMLOG_DOMAIN LOGD_DNS /*****************************************************************************/ #define _NMLOG(level, ...) __NMLOG_DEFAULT(level, _NMLOG_DOMAIN, "dnsmasq", __VA_ARGS__) #define WAIT_MSEC_AFTER_SIGTERM 1000 G_STATIC_ASSERT(WAIT_MSEC_AFTER_SIGTERM <= NM_SHUTDOWN_TIMEOUT_MS); #define WAIT_MSEC_AFTER_SIGKILL 400 G_STATIC_ASSERT(WAIT_MSEC_AFTER_SIGKILL + 100 <= NM_SHUTDOWN_TIMEOUT_MS_WATCHDOG); typedef void (*GlPidSpawnAsyncNotify)(GCancellable *cancellable, GPid pid, const int * p_exit_code, GError * error, gpointer notify_user_data); typedef struct { NMShutdownWaitObjHandle *shutdown_wait_handle; guint64 p_start_time; gint64 started_at; GPid pid; bool sigkilled : 1; } GlPidKillExternalData; typedef struct { const char * dm_binary; GlPidSpawnAsyncNotify notify; gpointer notify_user_data; GCancellable * cancellable; } GlPidSpawnAsyncData; static struct { GlPidKillExternalData *kill_external_data; GlPidSpawnAsyncData *spawn_data; NMShutdownWaitObjHandle *terminate_handle; GPid pid; guint terminate_timeout_id; guint watch_id; /* whether the external process (with the pid from PIDFILE) was already killed. * This only happens once, once we do that, we remember to not do it again. * The reason is that later one, when we want to kill the process it's a * child process. So, we wait for the exit code. */ bool kill_external_done : 1; bool terminate_sigkill : 1; } gl_pid; /*****************************************************************************/ static void _gl_pid_spawn_next_step(void); static void _gl_pid_spawn_cancelled_cb(GCancellable *cancellable, GlPidSpawnAsyncData *sdata); /*****************************************************************************/ static gboolean _gl_pid_unlink_pidfile(gboolean do_unlink) { int errsv; if (do_unlink) { if (unlink(PIDFILE) == 0) _LOGD("spawn: delete PID file %s", PIDFILE); else { errsv = errno; if (errsv != ENOENT) _LOGD("spawn: delete PID file %s failed: %s (%d)", PIDFILE, nm_strerror_native(errsv), errsv); } } return TRUE; } static gboolean _gl_pid_kill_external_timeout_cb(gpointer user_data) { guint64 p_start_time; char p_state = '\0'; gint64 now; p_start_time = nm_utils_get_start_time_for_pid(gl_pid.kill_external_data->pid, &p_state, NULL); if (p_start_time == 0 || p_start_time != gl_pid.kill_external_data->p_start_time || nm_utils_process_state_is_dead(p_state)) { _LOGD("spawn: process %" G_PID_FORMAT " from pidfile %s is gone", gl_pid.kill_external_data->pid, PIDFILE); goto process_gone; } now = nm_utils_get_monotonic_timestamp_msec(); if (gl_pid.kill_external_data->started_at + WAIT_MSEC_AFTER_SIGTERM < now) { if (!gl_pid.kill_external_data->sigkilled) { _LOGD("spawn: send SIGKILL to process %" G_PID_FORMAT " from pidfile %s", gl_pid.kill_external_data->pid, PIDFILE); gl_pid.kill_external_data->sigkilled = TRUE; kill(gl_pid.kill_external_data->pid, SIGKILL); } else if (gl_pid.kill_external_data->started_at + WAIT_MSEC_AFTER_SIGTERM + WAIT_MSEC_AFTER_SIGKILL < now) { _LOGW("spawn: process %" G_PID_FORMAT " from pidfile %s is still here after trying to kill it. Wait no longer", gl_pid.kill_external_data->pid, PIDFILE); goto process_gone; } } return G_SOURCE_CONTINUE; process_gone: nm_shutdown_wait_obj_unregister(gl_pid.kill_external_data->shutdown_wait_handle); g_slice_free(GlPidKillExternalData, g_steal_pointer(&gl_pid.kill_external_data)); _gl_pid_unlink_pidfile(TRUE); _gl_pid_spawn_next_step(); return G_SOURCE_REMOVE; } static gboolean _gl_pid_kill_external(void) { gs_free char *contents = NULL; gs_free char *cmdline_contents = NULL; gs_free_error GError *error = NULL; gint64 pid64; GPid pid = 0; guint64 p_start_time = 0; char proc_path[256]; gboolean do_kill = FALSE; char p_state = '\0'; gboolean do_unlink = TRUE; int errsv; if (gl_pid.kill_external_done) { if (gl_pid.kill_external_data) { _LOGD("spawn: waiting for external process %" G_PID_FORMAT " from pidfile %s quit", gl_pid.kill_external_data->pid, PIDFILE); return FALSE; } return TRUE; } if (!g_file_get_contents(PIDFILE, &contents, NULL, &error)) { if (g_error_matches(error, G_FILE_ERROR, G_FILE_ERROR_NOENT)) do_unlink = FALSE; _LOGD("spawn: failure to read pidfile %s: %s", PIDFILE, error->message); g_clear_error(&error); goto handle_kill; } pid64 = _nm_utils_ascii_str_to_int64(contents, 10, 2, G_MAXINT64, -1); if (pid64 == -1 || (pid = (GPid) pid64) != pid64) { _LOGD("spawn: pidfile %s does not contain a valid process identifier", PIDFILE); goto handle_kill; } G_STATIC_ASSERT_EXPR(sizeof(pid) == sizeof(pid_t)); p_start_time = nm_utils_get_start_time_for_pid(pid, &p_state, NULL); if (p_start_time == 0) { _LOGD("spawn: process %" G_PID_FORMAT " from pidfile %s seems to no longer exist", pid, PIDFILE); goto handle_kill; } nm_sprintf_buf(proc_path, "/proc/%" G_PID_FORMAT "/cmdline", pid); if (!g_file_get_contents(proc_path, &cmdline_contents, NULL, NULL)) { _LOGD("spawn: process %" G_PID_FORMAT " from pidfile %s seems to no longer exist", pid, PIDFILE); goto handle_kill; } if (!strstr(cmdline_contents, "/dnsmasq")) { _LOGD("spawn: process %" G_PID_FORMAT " from pidfile %s seems to no longer to be a dnsmasq process", pid, PIDFILE); goto handle_kill; } do_kill = TRUE; handle_kill: gl_pid.kill_external_done = TRUE; if (!do_kill) return _gl_pid_unlink_pidfile(do_unlink); if (nm_utils_process_state_is_dead(p_state)) { _LOGD("spawn: process %" G_PID_FORMAT " from pidfile %s is already a zombie", pid, PIDFILE); return _gl_pid_unlink_pidfile(do_unlink); } if (kill(pid, SIGTERM) != 0) { errsv = errno; if (errsv == ESRCH) _LOGD("spawn: process %" G_PID_FORMAT " from pidfile %s no longer exists", pid, PIDFILE); else _LOGD("spawn: process %" G_PID_FORMAT " from pidfile %s failed with \"%s\" (%d)", pid, PIDFILE, nm_strerror_native(errsv), errsv); return _gl_pid_unlink_pidfile(do_unlink); } _LOGD("spawn: waiting for process %" G_PID_FORMAT " from pidfile %s to terminate after SIGTERM", pid, PIDFILE); gl_pid.kill_external_data = g_slice_new(GlPidKillExternalData); *gl_pid.kill_external_data = (GlPidKillExternalData){ .shutdown_wait_handle = nm_shutdown_wait_obj_register_handle_full( g_strdup_printf("kill-external-dnsmasq-process-%" G_PID_FORMAT, pid), TRUE), .started_at = nm_utils_get_monotonic_timestamp_msec(), .pid = pid, .p_start_time = p_start_time, }; g_timeout_add(50, _gl_pid_kill_external_timeout_cb, NULL); return FALSE; } /*****************************************************************************/ static gboolean _gl_pid_spawn_clear_pid(void) { gboolean was_stopping = !!gl_pid.terminate_handle; gl_pid.pid = 0; gl_pid.terminate_sigkill = FALSE; nm_clear_g_source(&gl_pid.watch_id); nm_clear_g_source(&gl_pid.terminate_timeout_id); nm_clear_pointer(&gl_pid.terminate_handle, nm_shutdown_wait_obj_unregister); return was_stopping; } static void _gl_pid_spawn_register_for_termination(void) { if (gl_pid.pid > 0 && !gl_pid.terminate_handle) { /* Create a shutdown handle as a reminder that the currently running process must be terminated * first. This also happens to block shutdown... */ gl_pid.terminate_handle = nm_shutdown_wait_obj_register_handle_full( g_strdup_printf("kill-dnsmasq-process-%" G_PID_FORMAT, gl_pid.pid), TRUE); } } /** * _gl_pid_spawn_notify: * @sdata: the notify data. @sdata might be destroyed by the function, * depending on the other arguments (which indicate whether the * task is complete). * @pid: the PID to notify (argument for GlPidSpawnAsyncNotify) * @p_exit_code: the exit code to notify (argument for GlPidSpawnAsyncNotify) * @error: error reason to notify (argument for GlPidSpawnAsyncNotify) * * The GlPidSpawnAsyncNotify callback passed to _gl_pid_spawn() is used * for two purposes: * * - signal that the dnsmasq process was spawned (or failed to be spawned). * - signal that the dnsmasq process quit (if it was spawned successfully before). * * Depending on the arguments, the callee can see what's the case. */ static void _gl_pid_spawn_notify(GlPidSpawnAsyncData *sdata, GPid pid, const int *p_exit_code, GError *error) { gboolean destroy = TRUE; nm_assert(sdata); if (error) { nm_assert(pid == 0); nm_assert(!p_exit_code); if (!nm_utils_error_is_cancelled(error)) _LOGD("spawn: dnsmasq failed: %s", error->message); } else if (p_exit_code) { /* the only caller already logged about this condition extensively. */ nm_assert(pid > 0); } else { nm_assert(pid > 0); _LOGD("spawn: dnsmasq started with pid %" G_PID_FORMAT, pid); destroy = FALSE; } nm_assert((!!destroy) == (sdata != gl_pid.spawn_data)); if (destroy) g_signal_handlers_disconnect_by_func(sdata->cancellable, _gl_pid_spawn_cancelled_cb, sdata); sdata->notify(sdata->cancellable, pid, p_exit_code, error, sdata->notify_user_data); if (destroy) { g_clear_object(&sdata->cancellable); nm_g_slice_free(sdata); } } static void _gl_pid_spawn_cancelled_cb(GCancellable *cancellable, GlPidSpawnAsyncData *sdata) { gs_free_error GError *error = NULL; if (sdata == gl_pid.spawn_data) { gl_pid.spawn_data = NULL; /* When the cancellable gets cancelled, we terminate the current dnsmasq instance * in the background. The only way for keeping dnsmasq running while unregistering * the callback is by calling _gl_pid_spawn() without a new callback. */ _gl_pid_spawn_register_for_termination(); } else nm_assert_not_reached(); if (!g_cancellable_set_error_if_cancelled(cancellable, &error)) nm_assert_not_reached(); _gl_pid_spawn_notify(sdata, 0, NULL, error); _gl_pid_spawn_next_step(); } static gboolean _gl_pid_spawn_terminate_timeout_cb(gpointer user_data) { nm_assert(gl_pid.terminate_timeout_id != 0); nm_assert(gl_pid.pid > 0); nm_assert(gl_pid.terminate_handle); nm_assert(gl_pid.watch_id != 0); gl_pid.terminate_timeout_id = 0; if (!gl_pid.terminate_sigkill) { gl_pid.terminate_sigkill = TRUE; _LOGD("spawn: send SIGKILL signal to dnsmasq process %" G_PID_FORMAT " as it did not exit yet", gl_pid.pid); kill(gl_pid.pid, SIGKILL); gl_pid.terminate_timeout_id = g_timeout_add(WAIT_MSEC_AFTER_SIGKILL, _gl_pid_spawn_terminate_timeout_cb, NULL); } else { _LOGE("spawn: process %" G_PID_FORMAT " did not exit even after SIGTERM and SIGKILL", gl_pid.pid); /* we don't unregister the watch. Just forget about it. We still want to reap the child eventually. */ gl_pid.watch_id = 0; _gl_pid_spawn_clear_pid(); _gl_pid_spawn_next_step(); } return G_SOURCE_REMOVE; } static void _gl_pid_spawn_watch_cb(GPid pid, int status, gpointer user_data) { int err; gboolean was_stopping; nm_assert(pid > 0); if (WIFEXITED(status)) { err = WEXITSTATUS(status); if (err) { char sbuf[100]; _LOGW("spawn: dnsmasq process %" G_PID_FORMAT " exited with error: %s", pid, nm_utils_dnsmasq_status_to_string(err, sbuf, sizeof(sbuf))); } else _LOGD("spawn: dnsmasq process %" G_PID_FORMAT " exited normally", pid); } else if (WIFSTOPPED(status)) _LOGW("spawn: dnsmasq process %" G_PID_FORMAT " stopped unexpectedly with signal %d", pid, WSTOPSIG(status)); else if (WIFSIGNALED(status)) _LOGW("spawn: dnsmasq process %" G_PID_FORMAT " died with signal %d", pid, WTERMSIG(status)); else _LOGW("spawn: dnsmasq process %" G_PID_FORMAT " died from an unknown cause (status %d)", pid, status); if (gl_pid.pid != pid) { /* this can only happen, if we timed out and no longer care about this PID. * We still kept the watch-id active, to reap the process. Nothing to do. */ return; } nm_assert(gl_pid.watch_id != 0); gl_pid.watch_id = 0; _gl_pid_unlink_pidfile(TRUE); was_stopping = _gl_pid_spawn_clear_pid(); if (gl_pid.spawn_data) { if (was_stopping) { /* The current process was scheduled to be terminated. That means the pending * spawn_data is not for that former instance, but for starting a new one. * This spawn-request is not yet complete, instead it's just about to start. */ } else _gl_pid_spawn_notify(g_steal_pointer(&gl_pid.spawn_data), pid, &status, NULL); } _gl_pid_spawn_next_step(); } /** * _gl_pid_spawn_next_step: * * The state about a running dnsmasq process is tracked in @gl_pid. There are * various things that can happen * * - user calls _gl_pid_spawn() -- which might terminate an existing run first. * - user might cancel the GCancellable -- which would abort the spawning or * kill the current instance. * - the child process might exit. * * In all these cases, we call _gl_pid_spawn_next_step() to check what to do next. */ static void _gl_pid_spawn_next_step(void) { gs_free_error GError *error = NULL; const char * argv[15]; GPid pid = 0; guint argv_idx; if (!_gl_pid_kill_external()) { /* we need to wait to kill the instance from the PID file first. */ return; } if (gl_pid.terminate_handle) { nm_assert(gl_pid.pid > 0); if (gl_pid.terminate_timeout_id == 0) { _LOGD("spawn: send SIGTERM signal to process %" G_PID_FORMAT, gl_pid.pid); gl_pid.terminate_timeout_id = g_timeout_add(WAIT_MSEC_AFTER_SIGTERM, _gl_pid_spawn_terminate_timeout_cb, NULL); kill(gl_pid.pid, SIGTERM); } /* we can only wait for the process to exit. */ return; } if (!gl_pid.spawn_data) { /* we are not requested to spawn another process. */ nm_assert(gl_pid.pid == 0); return; } if (gl_pid.pid > 0) { /* the process we desire is already running. All good. */ return; } argv_idx = 0; argv[argv_idx++] = gl_pid.spawn_data->dm_binary; argv[argv_idx++] = "--no-resolv"; /* Use only commandline */ argv[argv_idx++] = "--keep-in-foreground"; argv[argv_idx++] = "--no-hosts"; /* don't use /etc/hosts to resolve */ argv[argv_idx++] = "--bind-interfaces"; argv[argv_idx++] = "--pid-file=" PIDFILE; argv[argv_idx++] = "--listen-address=127.0.0.1"; /* Should work for both 4 and 6 */ argv[argv_idx++] = "--cache-size=400"; argv[argv_idx++] = "--clear-on-reload"; /* clear cache when dns server changes */ argv[argv_idx++] = "--conf-file=/dev/null"; /* avoid loading /etc/dnsmasq.conf */ argv[argv_idx++] = "--proxy-dnssec"; /* Allow DNSSEC to pass through */ argv[argv_idx++] = "--enable-dbus=" DNSMASQ_DBUS_SERVICE; /* dnsmasq exits if the conf dir is not present */ if (g_file_test(CONFDIR, G_FILE_TEST_IS_DIR)) argv[argv_idx++] = "--conf-dir=" CONFDIR; argv[argv_idx++] = NULL; nm_assert(argv_idx <= G_N_ELEMENTS(argv)); if (!_LOGD_ENABLED()) _LOGI("starting %s", gl_pid.spawn_data->dm_binary); else { gs_free char *cmdline = NULL; _LOGD("spawn: starting dnsmasq: %s", (cmdline = g_strjoinv(" ", (char **) argv))); } if (!g_spawn_async(NULL, (char **) argv, NULL, G_SPAWN_DO_NOT_REAP_CHILD, nm_utils_setpgid, NULL, &pid, &error)) { _gl_pid_spawn_notify(g_steal_pointer(&gl_pid.spawn_data), 0, NULL, error); return; } gl_pid.pid = pid; gl_pid.watch_id = g_child_watch_add(pid, _gl_pid_spawn_watch_cb, NULL); _gl_pid_spawn_notify(gl_pid.spawn_data, pid, NULL, NULL); } /** * _gl_pid_spawn: * @dm_binary: the binary name for dnsmasq to spawn. We could * detect it ad-hoc right when needing it. But that would be * asynchronously, and if dnsmasq is not in $PATH, we want to * fail right away (synchronously). Hence, @dm_binary is * an argument. * @cancellable: abort the operation. This will invoke the callback * a last time. Also, if the dnsmasq process is currently running, * it will be terminated in the background. To unregister a notify * call without killing the dnsmasq process, call _gl_pid_spawn() * again with all arguments %NULL. * @notify: the callback when the process is started successfully * and when the process terminates. * @notify_user_data: user-data for callback. * * If a dnsmasq process is already running (from a previous call of * _gl_pid_spawn()), that one will be replaced. Meaning, the other notify * callback will be invoked with NM_UTILS_ERROR/NM_UTILS_ERROR_CANCELLED_DISPOSING. * If you the @dm_binary argument, the previously running process will * also be terminated first, before spawning a new instance. * However, you may also pass all arguments as %NULL. In that case, the * previous @notify will be completed (and forgotten), but the dnsmasq * process will be left running in the background. * * So, you can: * * - call _gl_pid_spawn() with a @dm_binary argument. The previous * notify() completes with NM_UTILS_ERROR_CANCELLED_DISPOSING and * the dnsmasq process gets killed. * - cancel the GCancellable, in this case the notify() completes * with G_IO_ERROR_CANCELLED and the dnsmasq process gets killed. * - call _gl_pid_spawn() with all arguments %NULL. In that case * the previous notify() completes with NM_UTILS_ERROR_CANCELLED_DISPOSING * but the dnsmasq process keeps running in the background. * * The callback is used in two cases. * - When spawning the process it will be invoked always exactly once. * In this case the callback might be invoked synchronously or * asynchronously. * This either provides a PID or a failure reason. In case of a * failure, that's the end and the process is not running. * - if the process could be spawned, the child process with the * provided PID gets monitored. When the process exits, the callback * will be invoked again, with a failure reason. This is always done * asynchronously. */ static void _gl_pid_spawn(const char * dm_binary, GCancellable * cancellable, GlPidSpawnAsyncNotify notify, gpointer notify_user_data) { GlPidSpawnAsyncData *sdata_replace; sdata_replace = g_steal_pointer(&gl_pid.spawn_data); if (dm_binary) { nm_assert(notify); nm_assert(G_IS_CANCELLABLE(cancellable)); gl_pid.spawn_data = g_slice_new(GlPidSpawnAsyncData); *gl_pid.spawn_data = (GlPidSpawnAsyncData){ .dm_binary = dm_binary, .notify = notify, .notify_user_data = notify_user_data, .cancellable = g_object_ref(cancellable), }; g_signal_connect(cancellable, "cancelled", G_CALLBACK(_gl_pid_spawn_cancelled_cb), gl_pid.spawn_data); /* If dnsmasq is running, we terminate it and start a new instance. * * If the user would not provide a new callback, this would mean to fail/abort * the currently subscribed notification (below). But it would leave the dnsmasq * instance running in the background. * This allows the user to say to not care about the current instance * anymore, but still leave it running. * * To kill the dnsmasq process without scheduling a new one, cancel the cancellable * instead. */ _gl_pid_spawn_register_for_termination(); } else { nm_assert(!notify); nm_assert(!cancellable); nm_assert(!notify_user_data); } if (sdata_replace) { gs_free_error GError *error = NULL; /* we don't mark the error as G_IO_ERROR/G_IO_ERROR_CANCELLED. That * is reserved for cancelling the cancellable. However, the current * request was obsoleted/replaced by a new one, so we fail it with * NM_UTILS_ERROR/NM_UTILS_ERROR_CANCELLED_DISPOSING. */ nm_utils_error_set_cancelled(&error, TRUE, NULL); _gl_pid_spawn_notify(sdata_replace, 0, NULL, error); } _gl_pid_spawn_next_step(); } /*****************************************************************************/ typedef struct { GDBusConnection *dbus_connection; GVariant *set_server_ex_args; GCancellable *update_cancellable; GCancellable *main_cancellable; char *name_owner; gint64 burst_start_at; GPid process_pid; guint name_owner_changed_id; guint main_timeout_id; guint burst_retry_timeout_id; guint8 burst_count; bool is_stopped : 1; } NMDnsDnsmasqPrivate; struct _NMDnsDnsmasq { NMDnsPlugin parent; NMDnsDnsmasqPrivate _priv; }; struct _NMDnsDnsmasqClass { NMDnsPluginClass parent; }; G_DEFINE_TYPE(NMDnsDnsmasq, nm_dns_dnsmasq, NM_TYPE_DNS_PLUGIN) #define NM_DNS_DNSMASQ_GET_PRIVATE(self) _NM_GET_PRIVATE(self, NMDnsDnsmasq, NM_IS_DNS_DNSMASQ) /*****************************************************************************/ #undef _NMLOG #define _NMLOG(level, ...) __NMLOG_DEFAULT_WITH_ADDR(level, _NMLOG_DOMAIN, "dnsmasq", __VA_ARGS__) /*****************************************************************************/ static gboolean start_dnsmasq(NMDnsDnsmasq *self, gboolean force_start, GError **error); /*****************************************************************************/ static void add_dnsmasq_nameserver(NMDnsDnsmasq * self, GVariantBuilder *servers, const char * ip, const char * domain) { g_return_if_fail(ip); _LOGD("adding nameserver '%s'%s%s%s", ip, NM_PRINT_FMT_QUOTED(domain, " for domain \"", domain, "\"", "")); g_variant_builder_open(servers, G_VARIANT_TYPE("as")); g_variant_builder_add(servers, "s", ip); if (domain) g_variant_builder_add(servers, "s", domain); g_variant_builder_close(servers); } #define IP_ADDR_TO_STRING_BUFLEN (NM_UTILS_INET_ADDRSTRLEN + 1 + IFNAMSIZ) static const char * ip_addr_to_string(int addr_family, gconstpointer addr, const char *iface, char *out_buf) { int n_written; char buf2[NM_UTILS_INET_ADDRSTRLEN]; const char *separator; nm_assert_addr_family(addr_family); nm_assert(addr); nm_assert(out_buf); if (addr_family == AF_INET) { nm_utils_inet_ntop(addr_family, addr, buf2); separator = "@"; } else { if (IN6_IS_ADDR_V4MAPPED(addr)) _nm_utils_inet4_ntop(((const struct in6_addr *) addr)->s6_addr32[3], buf2); else _nm_utils_inet6_ntop(addr, buf2); /* Need to scope link-local addresses with %. Before dnsmasq 2.58, * only '@' was supported as delimiter. Since 2.58, '@' and '%' are * supported. Due to a bug, since 2.73 only '%' works properly as "server" * address. */ separator = IN6_IS_ADDR_LINKLOCAL(addr) ? "%" : "@"; } n_written = g_snprintf(out_buf, IP_ADDR_TO_STRING_BUFLEN, "%s%s%s", buf2, iface ? separator : "", iface ?: ""); nm_assert(n_written < IP_ADDR_TO_STRING_BUFLEN); return out_buf; } static void add_global_config(NMDnsDnsmasq * self, GVariantBuilder * dnsmasq_servers, const NMGlobalDnsConfig *config) { guint i, j; g_return_if_fail(config); for (i = 0; i < nm_global_dns_config_get_num_domains(config); i++) { NMGlobalDnsDomain *domain = nm_global_dns_config_get_domain(config, i); const char *const *servers = nm_global_dns_domain_get_servers(domain); const char * name = nm_global_dns_domain_get_name(domain); g_return_if_fail(name); for (j = 0; servers && servers[j]; j++) { if (!strcmp(name, "*")) add_dnsmasq_nameserver(self, dnsmasq_servers, servers[j], NULL); else add_dnsmasq_nameserver(self, dnsmasq_servers, servers[j], name); } } } static void add_ip_config(NMDnsDnsmasq *self, GVariantBuilder *servers, const NMDnsIPConfigData *ip_data) { NMIPConfig * ip_config = ip_data->ip_config; gconstpointer addr; const char * iface, *domain; char ip_addr_to_string_buf[IP_ADDR_TO_STRING_BUFLEN]; int addr_family; guint i, j, num; iface = nm_platform_link_get_name(NM_PLATFORM_GET, ip_data->data->ifindex); addr_family = nm_ip_config_get_addr_family(ip_config); num = nm_ip_config_get_num_nameservers(ip_config); for (i = 0; i < num; i++) { addr = nm_ip_config_get_nameserver(ip_config, i); ip_addr_to_string(addr_family, addr, iface, ip_addr_to_string_buf); for (j = 0; ip_data->domains.search[j]; j++) { domain = nm_utils_parse_dns_domain(ip_data->domains.search[j], NULL); add_dnsmasq_nameserver(self, servers, ip_addr_to_string_buf, domain[0] ? domain : NULL); } if (ip_data->domains.reverse) { for (j = 0; ip_data->domains.reverse[j]; j++) { add_dnsmasq_nameserver(self, servers, ip_addr_to_string_buf, ip_data->domains.reverse[j]); } } } } static GVariant * create_update_args(NMDnsDnsmasq * self, const NMGlobalDnsConfig *global_config, const CList * ip_config_lst_head, const char * hostname) { GVariantBuilder servers; const NMDnsIPConfigData *ip_data; g_variant_builder_init(&servers, G_VARIANT_TYPE("aas")); if (global_config) add_global_config(self, &servers, global_config); else { c_list_for_each_entry (ip_data, ip_config_lst_head, ip_config_lst) add_ip_config(self, &servers, ip_data); } return g_variant_new("(aas)", &servers); } /*****************************************************************************/ static void dnsmasq_update_done(GObject *source_object, GAsyncResult *res, gpointer user_data) { NMDnsDnsmasq *self; gs_free_error GError *error = NULL; gs_unref_variant GVariant *response = NULL; response = g_dbus_connection_call_finish(G_DBUS_CONNECTION(source_object), res, &error); if (nm_utils_error_is_cancelled(error)) return; self = user_data; if (!response) _LOGW("dnsmasq update failed: %s", error->message); else _LOGD("dnsmasq update successful"); } static void send_dnsmasq_update(NMDnsDnsmasq *self) { NMDnsDnsmasqPrivate *priv = NM_DNS_DNSMASQ_GET_PRIVATE(self); if (!priv->name_owner || !priv->set_server_ex_args) return; _LOGD("trying to update dnsmasq nameservers"); nm_clear_g_cancellable(&priv->update_cancellable); priv->update_cancellable = g_cancellable_new(); g_dbus_connection_call(priv->dbus_connection, priv->name_owner, DNSMASQ_DBUS_PATH, DNSMASQ_DBUS_SERVICE, "SetServersEx", priv->set_server_ex_args, NULL, G_DBUS_CALL_FLAGS_NO_AUTO_START, 20000, priv->update_cancellable, dnsmasq_update_done, self); } /*****************************************************************************/ static void _main_cleanup(NMDnsDnsmasq *self, gboolean emit_failed) { NMDnsDnsmasqPrivate *priv = NM_DNS_DNSMASQ_GET_PRIVATE(self); if (!priv->main_cancellable) return; priv->process_pid = 0; nm_clear_g_free(&priv->name_owner); nm_clear_g_dbus_connection_signal(priv->dbus_connection, &priv->name_owner_changed_id); nm_clear_g_source(&priv->main_timeout_id); nm_clear_g_cancellable(&priv->update_cancellable); /* cancelling the main_cancellable will also cause _gl_pid_spawn*() to terminate the * process in the background. */ nm_clear_g_cancellable(&priv->main_cancellable); if (!priv->is_stopped && priv->burst_retry_timeout_id == 0) { start_dnsmasq(self, FALSE, NULL); send_dnsmasq_update(self); } } static void name_owner_changed(NMDnsDnsmasq *self, const char *name_owner) { NMDnsDnsmasqPrivate *priv = NM_DNS_DNSMASQ_GET_PRIVATE(self); name_owner = nm_str_not_empty(name_owner); if (nm_streq0(priv->name_owner, name_owner)) return; g_free(priv->name_owner); priv->name_owner = g_strdup(name_owner); if (!name_owner) { _LOGT("D-Bus name for dnsmasq disappeared"); _main_cleanup(self, TRUE); return; } _LOGT("D-Bus name for dnsmasq got owner %s", name_owner); nm_clear_g_source(&priv->main_timeout_id); send_dnsmasq_update(self); } static void name_owner_changed_cb(GDBusConnection *connection, const char * sender_name, const char * object_path, const char * interface_name, const char * signal_name, GVariant * parameters, gpointer user_data) { NMDnsDnsmasq *self = user_data; const char * new_owner; if (!g_variant_is_of_type(parameters, G_VARIANT_TYPE("(sss)"))) return; g_variant_get(parameters, "(&s&s&s)", NULL, NULL, &new_owner); name_owner_changed(self, new_owner); } static void get_name_owner_cb(const char *name_owner, GError *error, gpointer user_data) { if (!name_owner && g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) return; name_owner_changed(user_data, name_owner); } static gboolean spawn_timeout_cb(gpointer user_data) { NMDnsDnsmasq *self = user_data; _LOGW("timeout waiting for dnsmasq to appear on D-Bus"); _main_cleanup(self, TRUE); return G_SOURCE_REMOVE; } static void spawn_notify(GCancellable *cancellable, GPid pid, const int * p_exit_code, GError * error, gpointer notify_user_data) { NMDnsDnsmasq * self; NMDnsDnsmasqPrivate *priv; if (nm_utils_error_is_cancelled(error)) return; self = notify_user_data; priv = NM_DNS_DNSMASQ_GET_PRIVATE(self); if (error || p_exit_code) { _main_cleanup(self, TRUE); return; } nm_assert(pid > 0); priv->process_pid = pid; priv->name_owner_changed_id = nm_dbus_connection_signal_subscribe_name_owner_changed(priv->dbus_connection, DNSMASQ_DBUS_SERVICE, name_owner_changed_cb, self, NULL); nm_dbus_connection_call_get_name_owner(priv->dbus_connection, DNSMASQ_DBUS_SERVICE, -1, priv->main_cancellable, get_name_owner_cb, self); } static gboolean _burst_retry_timeout_cb(gpointer user_data) { NMDnsDnsmasq * self = user_data; NMDnsDnsmasqPrivate *priv = NM_DNS_DNSMASQ_GET_PRIVATE(self); priv->burst_retry_timeout_id = 0; start_dnsmasq(self, TRUE, NULL); send_dnsmasq_update(self); return G_SOURCE_REMOVE; } static gboolean start_dnsmasq(NMDnsDnsmasq *self, gboolean force_start, GError **error) { NMDnsDnsmasqPrivate *priv = NM_DNS_DNSMASQ_GET_PRIVATE(self); const char * dm_binary; gint64 now; if (G_LIKELY(priv->main_cancellable)) { /* The process is already running or about to be started. Nothing to do. */ return TRUE; } dm_binary = nm_utils_find_helper("dnsmasq", DNSMASQ_PATH, NULL); if (!dm_binary) { /* We resolve the binary name before trying to start it asynchronously. * The reason is, that if dnsmasq is not installed, we want to fail early, * so that NMDnsManager can fallback to a non-caching implementation. */ nm_utils_error_set(error, NM_UTILS_ERROR_UNKNOWN, "could not find dnsmasq binary"); return FALSE; } if (!priv->dbus_connection) { priv->dbus_connection = nm_g_object_ref(NM_MAIN_DBUS_CONNECTION_GET); if (!priv->dbus_connection) { nm_utils_error_set(error, NM_UTILS_ERROR_UNKNOWN, "no D-Bus connection available to talk to dnsmasq"); return FALSE; } } now = nm_utils_get_monotonic_timestamp_msec(); if (force_start || priv->burst_start_at == 0 || priv->burst_start_at + RATELIMIT_INTERVAL_MSEC <= now) { priv->burst_start_at = now; priv->burst_count = 1; nm_clear_g_source(&priv->burst_retry_timeout_id); _LOGT("rate-limit: start burst interval of %d seconds %s", RATELIMIT_INTERVAL_MSEC / 1000, force_start ? " (force)" : ""); } else if (priv->burst_count < RATELIMIT_BURST) { nm_assert(priv->burst_retry_timeout_id == 0); priv->burst_count++; _LOGT("rate-limit: %u try within burst interval of %d seconds", (guint) priv->burst_count, RATELIMIT_INTERVAL_MSEC / 1000); } else { if (priv->burst_retry_timeout_id == 0) { _LOGW("dnsmasq dies and gets respawned too quickly. Back off. Something is very wrong"); priv->burst_retry_timeout_id = g_timeout_add_seconds((2 * RATELIMIT_INTERVAL_MSEC) / 1000, _burst_retry_timeout_cb, self); } else _LOGT("rate-limit: currently rate-limited from restart"); return TRUE; } priv->main_timeout_id = g_timeout_add(10000, spawn_timeout_cb, self); priv->main_cancellable = g_cancellable_new(); _gl_pid_spawn(dm_binary, priv->main_cancellable, spawn_notify, self); return TRUE; } static gboolean update(NMDnsPlugin * plugin, const NMGlobalDnsConfig *global_config, const CList * ip_config_lst_head, const char * hostname, GError ** error) { NMDnsDnsmasq * self = NM_DNS_DNSMASQ(plugin); NMDnsDnsmasqPrivate *priv = NM_DNS_DNSMASQ_GET_PRIVATE(self); if (!start_dnsmasq(self, TRUE, error)) return FALSE; nm_clear_pointer(&priv->set_server_ex_args, g_variant_unref); priv->set_server_ex_args = g_variant_ref_sink(create_update_args(self, global_config, ip_config_lst_head, hostname)); send_dnsmasq_update(self); return TRUE; } /*****************************************************************************/ static void stop(NMDnsPlugin *plugin) { NMDnsDnsmasq * self = NM_DNS_DNSMASQ(plugin); NMDnsDnsmasqPrivate *priv = NM_DNS_DNSMASQ_GET_PRIVATE(self); priv->is_stopped = TRUE; priv->burst_start_at = 0; nm_clear_g_source(&priv->burst_retry_timeout_id); /* Cancelling the cancellable will also terminate the * process (in the background). */ _main_cleanup(self, FALSE); } /*****************************************************************************/ static void nm_dns_dnsmasq_init(NMDnsDnsmasq *self) {} NMDnsPlugin * nm_dns_dnsmasq_new(void) { return g_object_new(NM_TYPE_DNS_DNSMASQ, NULL); } static void dispose(GObject *object) { NMDnsDnsmasq * self = NM_DNS_DNSMASQ(object); NMDnsDnsmasqPrivate *priv = NM_DNS_DNSMASQ_GET_PRIVATE(self); priv->is_stopped = TRUE; nm_clear_g_source(&priv->burst_retry_timeout_id); _main_cleanup(self, FALSE); nm_clear_pointer(&priv->set_server_ex_args, g_variant_unref); G_OBJECT_CLASS(nm_dns_dnsmasq_parent_class)->dispose(object); g_clear_object(&priv->dbus_connection); } static void nm_dns_dnsmasq_class_init(NMDnsDnsmasqClass *dns_class) { NMDnsPluginClass *plugin_class = NM_DNS_PLUGIN_CLASS(dns_class); GObjectClass * object_class = G_OBJECT_CLASS(dns_class); object_class->dispose = dispose; plugin_class->plugin_name = "dnsmasq"; plugin_class->is_caching = TRUE; plugin_class->stop = stop; plugin_class->update = update; }