diff options
| author | Michael Biebl <biebl@debian.org> | 2021-02-11 18:11:46 +0100 |
|---|---|---|
| committer | Michael Biebl <biebl@debian.org> | 2021-02-11 18:11:46 +0100 |
| commit | 80ec1decc49c72efec2a8b87c06245c92c0ab807 (patch) | |
| tree | e3b229aa94e8dcf0590f2317664176e7b8f7607b /src/core/dns/nm-dns-dnsmasq.c | |
| parent | 65f86e8f56267192d42f2b629fc6b0c99fb9cd0c (diff) | |
New upstream version 1.29.90 upstream/1.29.90
Diffstat (limited to 'src/core/dns/nm-dns-dnsmasq.c')
| -rw-r--r-- | src/core/dns/nm-dns-dnsmasq.c | 1203 |
1 files changed, 1203 insertions, 0 deletions
diff --git a/src/core/dns/nm-dns-dnsmasq.c b/src/core/dns/nm-dns-dnsmasq.c new file mode 100644 index 00000000..dcff98e9 --- /dev/null +++ b/src/core/dns/nm-dns-dnsmasq.c @@ -0,0 +1,1203 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2010 Dan Williams <dcbw@redhat.com> + */ + +#include "src/core/nm-default-daemon.h" + +#include "nm-dns-dnsmasq.h" + +#include <stdlib.h> +#include <unistd.h> +#include <sys/types.h> +#include <sys/wait.h> +#include <arpa/inet.h> +#include <sys/stat.h> +#include <linux/if.h> + +#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 %<zone-id>. 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 NMDnsConfigIPData *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); + + if (!ip_data->domains.has_default_route_explicit && ip_data->domains.has_default_route) + add_dnsmasq_nameserver(self, servers, ip_addr_to_string_buf, NULL); + if (ip_data->domains.search) { + 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 NMDnsConfigIPData *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; +} |