/* SPDX-License-Identifier: GPL-2.0+ */ /* * Copyright (C) 2008 - 2012 Red Hat, Inc. */ #include "nm-default.h" #include "nm-dnsmasq-manager.h" #include #include #include #include #include #include #include "nm-dnsmasq-utils.h" #include "nm-utils.h" #include "NetworkManagerUtils.h" #include "nm-core-internal.h" #define CONFDIR NMCONFDIR "/dnsmasq-shared.d" /*****************************************************************************/ enum { STATE_CHANGED, LAST_SIGNAL, }; static guint signals[LAST_SIGNAL] = {0}; typedef struct { char *iface; char *pidfile; GPid pid; guint dm_watch_id; } NMDnsMasqManagerPrivate; struct _NMDnsMasqManager { GObject parent; NMDnsMasqManagerPrivate _priv; }; struct _NMDnsMasqManagerClass { GObjectClass parent; }; G_DEFINE_TYPE(NMDnsMasqManager, nm_dnsmasq_manager, G_TYPE_OBJECT) #define NM_DNSMASQ_MANAGER_GET_PRIVATE(self) \ _NM_GET_PRIVATE(self, NMDnsMasqManager, NM_IS_DNSMASQ_MANAGER) /*****************************************************************************/ #define _NMLOG_DOMAIN LOGD_SHARING #define _NMLOG(level, ...) __NMLOG_DEFAULT(level, _NMLOG_DOMAIN, "dnsmasq-manager", __VA_ARGS__) /*****************************************************************************/ static void dm_watch_cb(GPid pid, int status, gpointer user_data) { NMDnsMasqManager * manager = NM_DNSMASQ_MANAGER(user_data); NMDnsMasqManagerPrivate *priv = NM_DNSMASQ_MANAGER_GET_PRIVATE(manager); guint err; if (WIFEXITED(status)) { err = WEXITSTATUS(status); if (err != 0) { _LOGW("dnsmasq exited with error: %s", nm_utils_dnsmasq_status_to_string(err, NULL, 0)); } } 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->pid = 0; priv->dm_watch_id = 0; g_signal_emit(manager, signals[STATE_CHANGED], 0, NM_DNSMASQ_STATUS_DEAD); } static GPtrArray * create_dm_cmd_line(const char * iface, const NMIP4Config *ip4_config, const char * pidfile, gboolean announce_android_metered, GError ** error) { gs_unref_ptrarray GPtrArray *cmd = NULL; nm_auto_free_gstring GString *s = NULL; char first[INET_ADDRSTRLEN]; char last[INET_ADDRSTRLEN]; char listen_address_s[INET_ADDRSTRLEN]; char tmpaddr[INET_ADDRSTRLEN]; gs_free char * error_desc = NULL; const char * dm_binary; const NMPlatformIP4Address * listen_address; guint i, n; listen_address = nm_ip4_config_get_first_address(ip4_config); g_return_val_if_fail(listen_address, NULL); dm_binary = nm_utils_find_helper("dnsmasq", DNSMASQ_PATH, error); if (!dm_binary) return NULL; cmd = g_ptr_array_new_with_free_func(g_free); nm_strv_ptrarray_add_string_dup(cmd, dm_binary); if (nm_logging_enabled(LOGL_TRACE, LOGD_SHARING) || getenv("NM_DNSMASQ_DEBUG")) { nm_strv_ptrarray_add_string_dup(cmd, "--log-dhcp"); nm_strv_ptrarray_add_string_dup(cmd, "--log-queries"); } /* dnsmasq may read from its default config file location, which if that * location is a valid config file, it will combine with the options here * and cause undesirable side-effects. Like sending bogus IP addresses * as the gateway or whatever. So tell dnsmasq not to use any config file * at all. */ nm_strv_ptrarray_add_string_dup(cmd, "--conf-file=/dev/null"); nm_strv_ptrarray_add_string_dup(cmd, "--no-hosts"); nm_strv_ptrarray_add_string_dup(cmd, "--keep-in-foreground"); nm_strv_ptrarray_add_string_dup(cmd, "--bind-interfaces"); nm_strv_ptrarray_add_string_dup(cmd, "--except-interface=lo"); nm_strv_ptrarray_add_string_dup(cmd, "--clear-on-reload"); /* Use strict order since in the case of VPN connections, the VPN's * nameservers will be first in resolv.conf, and those need to be tried * first by dnsmasq to successfully resolve names from the VPN. */ nm_strv_ptrarray_add_string_dup(cmd, "--strict-order"); _nm_utils_inet4_ntop(listen_address->address, listen_address_s); nm_strv_ptrarray_add_string_concat(cmd, "--listen-address=", listen_address_s); if (!nm_dnsmasq_utils_get_range(listen_address, first, last, &error_desc)) { g_set_error_literal(error, NM_MANAGER_ERROR, NM_MANAGER_ERROR_FAILED, error_desc); _LOGW("failed to find DHCP address ranges: %s", error_desc); return NULL; } nm_strv_ptrarray_add_string_printf(cmd, "--dhcp-range=%s,%s,60m", first, last); if (nm_ip4_config_best_default_route_get(ip4_config)) { nm_strv_ptrarray_add_string_concat(cmd, "--dhcp-option=option:router,", listen_address_s); } if ((n = nm_ip4_config_get_num_nameservers(ip4_config))) { nm_gstring_prepare(&s); g_string_append(s, "--dhcp-option=option:dns-server"); for (i = 0; i < n; i++) { g_string_append_c(s, ','); g_string_append( s, _nm_utils_inet4_ntop(nm_ip4_config_get_nameserver(ip4_config, i), tmpaddr)); } nm_strv_ptrarray_take_gstring(cmd, &s); } if ((n = nm_ip4_config_get_num_searches(ip4_config))) { nm_gstring_prepare(&s); g_string_append(s, "--dhcp-option=option:domain-search"); for (i = 0; i < n; i++) { g_string_append_c(s, ','); g_string_append(s, nm_ip4_config_get_search(ip4_config, i)); } nm_strv_ptrarray_take_gstring(cmd, &s); } if (announce_android_metered) { /* force option 43 to announce ANDROID_METERED. Do this, even if the client * did not ask for this option. See https://www.lorier.net/docs/android-metered.html */ nm_strv_ptrarray_add_string_dup(cmd, "--dhcp-option-force=43,ANDROID_METERED"); } nm_strv_ptrarray_add_string_dup(cmd, "--dhcp-lease-max=50"); nm_strv_ptrarray_add_string_printf(cmd, "--dhcp-leasefile=%s/dnsmasq-%s.leases", NMSTATEDIR, iface); nm_strv_ptrarray_add_string_concat(cmd, "--pid-file=", pidfile); /* dnsmasq exits if the conf dir is not present */ if (g_file_test(CONFDIR, G_FILE_TEST_IS_DIR)) nm_strv_ptrarray_add_string_dup(cmd, "--conf-dir=" CONFDIR); g_ptr_array_add(cmd, NULL); return g_steal_pointer(&cmd); } static void kill_existing_by_pidfile(const char *pidfile) { char * contents = NULL; pid_t pid; char proc_path[250]; char * cmdline_contents = NULL; guint64 start_time; const char *exe; if (!pidfile || !g_file_get_contents(pidfile, &contents, NULL, NULL)) return; pid = _nm_utils_ascii_str_to_int64(contents, 10, 1, G_MAXUINT64, 0); if (pid == 0) goto out; start_time = nm_utils_get_start_time_for_pid(pid, NULL, NULL); if (start_time == 0) goto out; nm_sprintf_buf(proc_path, "/proc/%lld/cmdline", (long long) pid); if (!g_file_get_contents(proc_path, &cmdline_contents, NULL, NULL)) goto out; exe = strrchr(cmdline_contents, '/'); if ((exe && strcmp(&exe[1], "dnsmasq") == 0) || (strcmp(cmdline_contents, DNSMASQ_PATH) == 0)) { nm_utils_kill_process_sync(pid, start_time, SIGKILL, LOGD_SHARING, "dnsmasq", 0, 0, 500); } out: unlink(pidfile); g_free(cmdline_contents); g_free(contents); } gboolean nm_dnsmasq_manager_start(NMDnsMasqManager *manager, NMIP4Config * ip4_config, gboolean announce_android_metered, GError ** error) { NMDnsMasqManagerPrivate *priv; gs_unref_ptrarray GPtrArray *dm_cmd = NULL; gs_free char * cmd_str = NULL; g_return_val_if_fail(NM_IS_DNSMASQ_MANAGER(manager), FALSE); g_return_val_if_fail(!error || !*error, FALSE); g_return_val_if_fail(nm_ip4_config_get_num_addresses(ip4_config) > 0, FALSE); priv = NM_DNSMASQ_MANAGER_GET_PRIVATE(manager); kill_existing_by_pidfile(priv->pidfile); dm_cmd = create_dm_cmd_line(priv->iface, ip4_config, priv->pidfile, announce_android_metered, error); if (!dm_cmd) return FALSE; _LOGI("starting dnsmasq..."); _LOGD("command line: %s", (cmd_str = g_strjoinv(" ", (char **) dm_cmd->pdata))); priv->pid = 0; if (!g_spawn_async(NULL, (char **) dm_cmd->pdata, NULL, G_SPAWN_DO_NOT_REAP_CHILD, nm_utils_setpgid, NULL, &priv->pid, error)) return FALSE; nm_assert(priv->pid > 0); _LOGD("dnsmasq started with pid %d", priv->pid); priv->dm_watch_id = g_child_watch_add(priv->pid, (GChildWatchFunc) dm_watch_cb, manager); return TRUE; } void nm_dnsmasq_manager_stop(NMDnsMasqManager *manager) { NMDnsMasqManagerPrivate *priv; g_return_if_fail(NM_IS_DNSMASQ_MANAGER(manager)); priv = NM_DNSMASQ_MANAGER_GET_PRIVATE(manager); nm_clear_g_source(&priv->dm_watch_id); if (priv->pid) { nm_utils_kill_child_async(priv->pid, SIGTERM, LOGD_SHARING, "dnsmasq", 2000, NULL, NULL); priv->pid = 0; } unlink(priv->pidfile); } /*****************************************************************************/ static void nm_dnsmasq_manager_init(NMDnsMasqManager *manager) {} NMDnsMasqManager * nm_dnsmasq_manager_new(const char *iface) { NMDnsMasqManager * manager; NMDnsMasqManagerPrivate *priv; manager = g_object_new(NM_TYPE_DNSMASQ_MANAGER, NULL); priv = NM_DNSMASQ_MANAGER_GET_PRIVATE(manager); priv->iface = g_strdup(iface); priv->pidfile = g_strdup_printf(RUNSTATEDIR "/nm-dnsmasq-%s.pid", iface); return manager; } static void finalize(GObject *object) { NMDnsMasqManagerPrivate *priv = NM_DNSMASQ_MANAGER_GET_PRIVATE(object); nm_dnsmasq_manager_stop(NM_DNSMASQ_MANAGER(object)); g_free(priv->iface); g_free(priv->pidfile); G_OBJECT_CLASS(nm_dnsmasq_manager_parent_class)->finalize(object); } static void nm_dnsmasq_manager_class_init(NMDnsMasqManagerClass *manager_class) { GObjectClass *object_class = G_OBJECT_CLASS(manager_class); object_class->finalize = finalize; signals[STATE_CHANGED] = g_signal_new(NM_DNS_MASQ_MANAGER_STATE_CHANGED, G_OBJECT_CLASS_TYPE(object_class), G_SIGNAL_RUN_FIRST, 0, NULL, NULL, g_cclosure_marshal_VOID__UINT, G_TYPE_NONE, 1, G_TYPE_UINT); }