/* SPDX-License-Identifier: GPL-2.0-or-later */
/*
 * Copyright (C) 2008 - 2012 Red Hat, Inc.
 */

#include "src/core/nm-default-daemon.h"

#include "nm-dnsmasq-manager.h"

#include <sys/types.h>
#include <sys/wait.h>
#include <signal.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <stdlib.h>

#include "nm-dnsmasq-utils.h"
#include "nm-utils.h"
#include "NetworkManagerUtils.h"
#include "libnm-core-intern/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);
}
