diff options
Diffstat (limited to 'src/dns/nm-dns-dnsmasq.c')
| -rw-r--r-- | src/dns/nm-dns-dnsmasq.c | 1094 |
1 files changed, 892 insertions, 202 deletions
diff --git a/src/dns/nm-dns-dnsmasq.c b/src/dns/nm-dns-dnsmasq.c index 13576c92..a5028e42 100644 --- a/src/dns/nm-dns-dnsmasq.c +++ b/src/dns/nm-dns-dnsmasq.c @@ -1,20 +1,6 @@ +// SPDX-License-Identifier: GPL-2.0+ /* * Copyright (C) 2010 Dan Williams <dcbw@redhat.com> - * - * 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; either version 2, or (at your option) - * any later version. - * - * 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, write to the Free Software Foundation, Inc., - * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - * */ #include "nm-default.h" @@ -29,6 +15,7 @@ #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" @@ -37,21 +24,644 @@ #include "nm-dbus-manager.h" #include "NetworkManagerUtils.h" -#define PIDFILE NMRUNDIR "/dnsmasq.pid" -#define CONFDIR NMCONFDIR "/dnsmasq.d" +#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 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 { - GDBusProxy *dnsmasq; - GCancellable *dnsmasq_cancellable; - GCancellable *update_cancellable; - gboolean running; + 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_ms (); + + 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_ms (), + .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 shtudown 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 sucessfully 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, FALSE)) + _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 (synchrounously). 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 { @@ -69,11 +679,15 @@ G_DEFINE_TYPE (NMDnsDnsmasq, nm_dns_dnsmasq, NM_TYPE_DNS_PLUGIN) /*****************************************************************************/ -#define _NMLOG_DOMAIN LOGD_DNS +#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, @@ -192,19 +806,42 @@ add_ip_config (NMDnsDnsmasq *self, GVariantBuilder *servers, const NMDnsIPConfig } } +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 (GDBusProxy *proxy, GAsyncResult *res, gpointer user_data) +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_proxy_call_finish (proxy, res, &error); - if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) - return; + response = g_dbus_connection_call_finish (G_DBUS_CONNECTION (source_object), res, &error); - self = NM_DNS_DNSMASQ (user_data); + if (nm_utils_error_is_cancelled (error, FALSE)) + return; + self = user_data; if (!response) _LOGW ("dnsmasq update failed: %s", error->message); else @@ -216,238 +853,287 @@ send_dnsmasq_update (NMDnsDnsmasq *self) { NMDnsDnsmasqPrivate *priv = NM_DNS_DNSMASQ_GET_PRIVATE (self); - if (!priv->set_server_ex_args) - return; + if ( !priv->name_owner + || !priv->set_server_ex_args) + return; - if (priv->running) { - _LOGD ("trying to update dnsmasq nameservers"); - - nm_clear_g_cancellable (&priv->update_cancellable); - priv->update_cancellable = g_cancellable_new (); - - g_dbus_proxy_call (priv->dnsmasq, - "SetServersEx", - priv->set_server_ex_args, - G_DBUS_CALL_FLAGS_NONE, - -1, - priv->update_cancellable, - (GAsyncReadyCallback) dnsmasq_update_done, - self); - g_clear_pointer (&priv->set_server_ex_args, g_variant_unref); - } else - _LOGD ("dnsmasq not found on the bus. The nameserver update will be sent when dnsmasq appears"); + _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 -name_owner_changed (GObject *object, - GParamSpec *pspec, - gpointer user_data) +_main_cleanup (NMDnsDnsmasq *self, gboolean emit_failed) { - NMDnsDnsmasq *self = NM_DNS_DNSMASQ (user_data); NMDnsDnsmasqPrivate *priv = NM_DNS_DNSMASQ_GET_PRIVATE (self); - gs_free char *owner = NULL; - owner = g_dbus_proxy_get_name_owner (G_DBUS_PROXY (object)); - if (owner) { - _LOGI ("dnsmasq appeared as %s", owner); - priv->running = TRUE; + 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); - } else { - if (priv->running) { - _LOGI ("dnsmasq disappeared"); - priv->running = FALSE; - g_signal_emit_by_name (self, NM_DNS_PLUGIN_FAILED); - } else { - /* The only reason for which (!priv->running) here - * is that the dnsmasq process quit. We don't care - * of that here, the manager handles child restarts - * by itself. */ - } } } static void -dnsmasq_proxy_cb (GObject *source, GAsyncResult *res, gpointer user_data) +name_owner_changed (NMDnsDnsmasq *self, + const char *name_owner) { - NMDnsDnsmasq *self; - NMDnsDnsmasqPrivate *priv; - gs_free_error GError *error = NULL; - gs_free char *owner = NULL; - GDBusProxy *proxy; + NMDnsDnsmasqPrivate *priv = NM_DNS_DNSMASQ_GET_PRIVATE (self); - proxy = g_dbus_proxy_new_finish (res, &error); - if ( !proxy - && g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + name_owner = nm_str_not_empty (name_owner); + + if (nm_streq0 (priv->name_owner, name_owner)) return; - self = NM_DNS_DNSMASQ (user_data); + g_free (priv->name_owner); + priv->name_owner = g_strdup (name_owner); - if (!proxy) { - _LOGW ("failed to connect to dnsmasq via DBus: %s", error->message); - g_signal_emit_by_name (self, NM_DNS_PLUGIN_FAILED); + if (!name_owner) { + _LOGT ("D-Bus name for dnsmasq disappeared"); + _main_cleanup (self, TRUE); return; } - priv = NM_DNS_DNSMASQ_GET_PRIVATE (self); + _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; - priv->dnsmasq = proxy; - nm_clear_g_cancellable (&priv->dnsmasq_cancellable); + if (!g_variant_is_of_type (parameters, G_VARIANT_TYPE ("(sss)"))) + return; - _LOGD ("dnsmasq proxy creation successful"); + g_variant_get (parameters, + "(&s&s&s)", + NULL, + NULL, + &new_owner); - g_signal_connect (priv->dnsmasq, "notify::g-name-owner", - G_CALLBACK (name_owner_changed), self); - owner = g_dbus_proxy_get_name_owner (priv->dnsmasq); - priv->running = (owner != NULL); + name_owner_changed (self, new_owner); +} - if (priv->running) - send_dnsmasq_update (self); +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 -start_dnsmasq (NMDnsDnsmasq *self) +spawn_notify (GCancellable *cancellable, + GPid pid, + const int *p_exit_code, + GError *error, + gpointer notify_user_data) { - NMDnsDnsmasqPrivate *priv = NM_DNS_DNSMASQ_GET_PRIVATE (self); - const char *dm_binary; - const char *argv[15]; - GPid pid = 0; - guint idx = 0; + NMDnsDnsmasq *self; + NMDnsDnsmasqPrivate *priv; - if (priv->running) { - /* the dnsmasq process is running. Nothing to do. */ + if (nm_utils_error_is_cancelled (error, FALSE)) return; - } - if (nm_dns_plugin_child_pid ((NMDnsPlugin *) self) > 0) { - /* if we already have a child process spawned, don't do - * it again. */ + 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) { - _LOGW ("could not find dnsmasq binary"); - return; + /* 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; } - argv[idx++] = dm_binary; - argv[idx++] = "--no-resolv"; /* Use only commandline */ - argv[idx++] = "--keep-in-foreground"; - argv[idx++] = "--no-hosts"; /* don't use /etc/hosts to resolve */ - argv[idx++] = "--bind-interfaces"; - argv[idx++] = "--pid-file=" PIDFILE; - argv[idx++] = "--listen-address=127.0.0.1"; /* Should work for both 4 and 6 */ - argv[idx++] = "--cache-size=400"; - argv[idx++] = "--clear-on-reload"; /* clear cache when dns server changes */ - argv[idx++] = "--conf-file=/dev/null"; /* avoid loading /etc/dnsmasq.conf */ - argv[idx++] = "--proxy-dnssec"; /* Allow DNSSEC to pass through */ - 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[idx++] = "--conf-dir=" CONFDIR; + 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; + } + } - argv[idx++] = NULL; - nm_assert (idx <= G_N_ELEMENTS (argv)); + now = nm_utils_get_monotonic_timestamp_ms (); + 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; + } - /* And finally spawn dnsmasq */ - pid = nm_dns_plugin_child_spawn (NM_DNS_PLUGIN (self), argv, PIDFILE, "bin/dnsmasq"); - if (!pid) - return; + priv->main_timeout_id = g_timeout_add (10000, + spawn_timeout_cb, + self); - if ( priv->dnsmasq - || priv->dnsmasq_cancellable) { - /* we already have a proxy or are about to create it. - * We are done. */ - return; - } + priv->main_cancellable = g_cancellable_new (); - priv->dnsmasq_cancellable = g_cancellable_new (); - g_dbus_proxy_new_for_bus (G_BUS_TYPE_SYSTEM, - G_DBUS_PROXY_FLAGS_DO_NOT_AUTO_START, - NULL, - DNSMASQ_DBUS_SERVICE, - DNSMASQ_DBUS_PATH, - DNSMASQ_DBUS_SERVICE, - priv->dnsmasq_cancellable, - dnsmasq_proxy_cb, - self); + _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) + const char *hostname, + GError **error) { NMDnsDnsmasq *self = NM_DNS_DNSMASQ (plugin); NMDnsDnsmasqPrivate *priv = NM_DNS_DNSMASQ_GET_PRIVATE (self); - GVariantBuilder servers; - const NMDnsIPConfigData *ip_data; - - start_dnsmasq (self); - - 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); - } + if (!start_dnsmasq (self, TRUE, error)) + return FALSE; - g_clear_pointer (&priv->set_server_ex_args, g_variant_unref); - priv->set_server_ex_args = g_variant_ref_sink (g_variant_new ("(aas)", &servers)); + 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 -child_quit (NMDnsPlugin *plugin, int status) +stop (NMDnsPlugin *plugin) { NMDnsDnsmasq *self = NM_DNS_DNSMASQ (plugin); NMDnsDnsmasqPrivate *priv = NM_DNS_DNSMASQ_GET_PRIVATE (self); - gboolean failed = TRUE; - int err; - - if (WIFEXITED (status)) { - err = WEXITSTATUS (status); - if (err) { - _LOGW ("dnsmasq exited with error: %s", - nm_utils_dnsmasq_status_to_string (err, NULL, 0)); - } else { - _LOGD ("dnsmasq exited normally"); - failed = FALSE; - } - } else if (WIFSTOPPED (status)) - _LOGW ("dnsmasq stopped unexpectedly with signal %d", WSTOPSIG (status)); - else if (WIFSIGNALED (status)) - _LOGW ("dnsmasq died with signal %d", WTERMSIG (status)); - else - _LOGW ("dnsmasq died from an unknown cause"); - - priv->running = FALSE; - - if (failed) - g_signal_emit_by_name (self, NM_DNS_PLUGIN_FAILED); -} - -/*****************************************************************************/ -static gboolean -is_caching (NMDnsPlugin *plugin) -{ - return TRUE; -} + priv->is_stopped = TRUE; + priv->burst_start_at = 0; + nm_clear_g_source (&priv->burst_retry_timeout_id); -static const char * -get_name (NMDnsPlugin *plugin) -{ - return "dnsmasq"; + /* Cancelling the cancellable will also terminate the + * process (in the background). */ + _main_cleanup (self, FALSE); } /*****************************************************************************/ @@ -466,16 +1152,20 @@ nm_dns_dnsmasq_new (void) static void dispose (GObject *object) { - NMDnsDnsmasqPrivate *priv = NM_DNS_DNSMASQ_GET_PRIVATE ((NMDnsDnsmasq *) object); + NMDnsDnsmasq *self = NM_DNS_DNSMASQ (object); + NMDnsDnsmasqPrivate *priv = NM_DNS_DNSMASQ_GET_PRIVATE (self); - nm_clear_g_cancellable (&priv->dnsmasq_cancellable); - nm_clear_g_cancellable (&priv->update_cancellable); + priv->is_stopped = TRUE; - g_clear_object (&priv->dnsmasq); + nm_clear_g_source (&priv->burst_retry_timeout_id); + + _main_cleanup (self, FALSE); g_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 @@ -486,8 +1176,8 @@ nm_dns_dnsmasq_class_init (NMDnsDnsmasqClass *dns_class) object_class->dispose = dispose; - plugin_class->child_quit = child_quit; - plugin_class->is_caching = is_caching; - plugin_class->update = update; - plugin_class->get_name = get_name; + plugin_class->plugin_name = "dnsmasq"; + plugin_class->is_caching = TRUE; + plugin_class->stop = stop; + plugin_class->update = update; } |