/* SPDX-License-Identifier: LGPL-2.1-or-later */
/*
 * Copyright (C) 2014 Red Hat, Inc.
 */

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

#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <ctype.h>
#include <net/if_arp.h>

#include "libnm-glib-aux/nm-dedup-multi.h"
#include "libnm-std-aux/unaligned.h"

#include "nm-utils.h"
#include "nm-l3-config-data.h"
#include "nm-dhcp-utils.h"
#include "nm-dhcp-options.h"
#include "nm-core-utils.h"
#include "NetworkManagerUtils.h"
#include "libnm-platform/nm-platform.h"
#include "nm-dhcp-client-logging.h"
#include "libnm-systemd-core/nm-sd.h"

/*****************************************************************************/

#define NM_TYPE_DHCP_SYSTEMD (nm_dhcp_systemd_get_type())
#define NM_DHCP_SYSTEMD(obj) \
    (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DHCP_SYSTEMD, NMDhcpSystemd))
#define NM_DHCP_SYSTEMD_CLASS(klass) \
    (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_DHCP_SYSTEMD, NMDhcpSystemdClass))
#define NM_IS_DHCP_SYSTEMD(obj)         (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_DHCP_SYSTEMD))
#define NM_IS_DHCP_SYSTEMD_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), NM_TYPE_DHCP_SYSTEMD))
#define NM_DHCP_SYSTEMD_GET_CLASS(obj) \
    (G_TYPE_INSTANCE_GET_CLASS((obj), NM_TYPE_DHCP_SYSTEMD, NMDhcpSystemdClass))

typedef struct _NMDhcpSystemd      NMDhcpSystemd;
typedef struct _NMDhcpSystemdClass NMDhcpSystemdClass;

static GType nm_dhcp_systemd_get_type(void);

/*****************************************************************************/

typedef struct {
    sd_dhcp6_client *client6;
    char            *lease_file;

    guint request_count;
} NMDhcpSystemdPrivate;

struct _NMDhcpSystemd {
    NMDhcpClient         parent;
    NMDhcpSystemdPrivate _priv;
};

struct _NMDhcpSystemdClass {
    NMDhcpClientClass parent;
};

G_DEFINE_TYPE(NMDhcpSystemd, nm_dhcp_systemd, NM_TYPE_DHCP_CLIENT)

#define NM_DHCP_SYSTEMD_GET_PRIVATE(self) _NM_GET_PRIVATE(self, NMDhcpSystemd, NM_IS_DHCP_SYSTEMD)

/*****************************************************************************/

static guint32
lifetime_to_uint32(guint64 lft)
{
    if (lft == G_MAXUINT64)
        return G_MAXUINT32;

    return lft / 1000000;
}

static NML3ConfigData *
lease_to_ip6_config(NMDhcpSystemd *self, sd_dhcp6_lease *lease, gint32 ts, GError **error)
{
    const NMDhcpClientConfig               *config;
    nm_auto_unref_l3cd_init NML3ConfigData *l3cd    = NULL;
    gs_unref_hashtable GHashTable          *options = NULL;
    struct in6_addr                         tmp_addr;
    const struct in6_addr                  *dns;
    char                                    addr_str[NM_INET_ADDRSTRLEN];
    char                                    iaid_buf[NM_DHCP_IAID_TO_HEXSTR_BUF_LEN];
    char                                  **domains;
    char                                  **ntp_fqdns;
    const struct in6_addr                  *ntp_addrs;
    const char                             *s;
    nm_auto_free_gstring GString           *str = NULL;
    int                                     num, i;

    nm_assert(lease);

    config = nm_dhcp_client_get_config(NM_DHCP_CLIENT(self));

    l3cd = nm_dhcp_client_create_l3cd(NM_DHCP_CLIENT(self));

    options = nm_dhcp_client_create_options_dict(NM_DHCP_CLIENT(self), TRUE);

    nm_dhcp_option_add_option(options,
                              TRUE,
                              AF_INET6,
                              NM_DHCP_OPTION_DHCP6_NM_IAID,
                              nm_dhcp_iaid_to_hexstr(config->v6.iaid, iaid_buf));

    if (!config->v6.info_only) {
        gboolean has_any_addresses = FALSE;
        uint64_t lft_pref;
        uint64_t lft_valid;

        sd_dhcp6_lease_address_iterator_reset(lease);
        nm_gstring_prepare(&str);
        while (sd_dhcp6_lease_get_address(lease, &tmp_addr) >= 0
               && sd_dhcp6_lease_get_address_lifetime(lease, &lft_pref, &lft_valid) >= 0) {
            NMPlatformIP6Address address = {
                .plen        = 128,
                .address     = tmp_addr,
                .timestamp   = ts,
                .lifetime    = lifetime_to_uint32(lft_valid),
                .preferred   = lifetime_to_uint32(lft_pref),
                .addr_source = NM_IP_CONFIG_SOURCE_DHCP,
            };

            nm_l3_config_data_add_address_6(l3cd, &address);

            nm_inet6_ntop(&tmp_addr, addr_str);
            g_string_append(nm_gstring_add_space_delimiter(str), addr_str);

            has_any_addresses = TRUE;
            sd_dhcp6_lease_address_iterator_next(lease);
        }

        if (str->len) {
            nm_dhcp_option_add_option(options,
                                      TRUE,
                                      AF_INET6,
                                      NM_DHCP_OPTION_DHCP6_NM_IP_ADDRESS,
                                      str->str);
        }

        if (!has_any_addresses) {
            g_set_error_literal(error,
                                NM_MANAGER_ERROR,
                                NM_MANAGER_ERROR_FAILED,
                                "no address received in managed mode");
            return NULL;
        }
    }

    num = sd_dhcp6_lease_get_dns(lease, &dns);
    if (num > 0) {
        nm_gstring_prepare(&str);
        for (i = 0; i < num; i++) {
            nm_inet6_ntop(&dns[i], addr_str);
            g_string_append(nm_gstring_add_space_delimiter(str), addr_str);
            nm_l3_config_data_add_nameserver_addr(l3cd, AF_INET6, &dns[i]);
        }
        nm_dhcp_option_add_option(options,
                                  TRUE,
                                  AF_INET6,
                                  NM_DHCP_OPTION_DHCP6_DNS_SERVERS,
                                  str->str);
    }

    {
        struct in6_addr prefix;
        uint8_t         prefix_len;

        nm_gstring_prepare(&str);
        sd_dhcp6_lease_pd_iterator_reset(lease);
        while (!sd_dhcp6_lease_get_pd_prefix(lease, &prefix, &prefix_len)) {
            nm_gstring_add_space_delimiter(str);
            nm_inet6_ntop(&prefix, addr_str);
            g_string_append_printf(str, "%s/%u", addr_str, prefix_len);
            sd_dhcp6_lease_pd_iterator_next(lease);
        }
        if (str->len > 0) {
            nm_dhcp_option_add_option(options,
                                      TRUE,
                                      AF_INET6,
                                      NM_DHCP_OPTION_DHCP6_IA_PD,
                                      str->str);
        }
    }

    num = sd_dhcp6_lease_get_domains(lease, &domains);
    if (num > 0) {
        nm_gstring_prepare(&str);
        for (i = 0; i < num; i++) {
            g_string_append(nm_gstring_add_space_delimiter(str), domains[i]);
            nm_l3_config_data_add_search(l3cd, AF_INET6, domains[i]);
        }
        nm_dhcp_option_add_option(options,
                                  TRUE,
                                  AF_INET6,
                                  NM_DHCP_OPTION_DHCP6_DOMAIN_LIST,
                                  str->str);
    }

    if (sd_dhcp6_lease_get_fqdn(lease, &s) >= 0) {
        nm_dhcp_option_add_option(options, TRUE, AF_INET6, NM_DHCP_OPTION_DHCP6_FQDN, s);
    }

    /* RFC 5908, section 4 states: "This option MUST include one, and only
     * one, time source suboption." It is not clear why systemd chose to
     * return array of addresses and FQDNs. Given there seem to be no
     * technical obstacles to including multiple options, let's just
     * pass on whatever systemd tells us.
     */
    nm_gstring_prepare(&str);
    num = sd_dhcp6_lease_get_ntp_fqdn(lease, &ntp_fqdns);
    if (num > 0) {
        for (i = 0; i < num; i++) {
            g_string_append(nm_gstring_add_space_delimiter(str), ntp_fqdns[i]);
        }
    }
    num = sd_dhcp6_lease_get_ntp_addrs(lease, &ntp_addrs);
    if (num > 0) {
        for (i = 0; i < num; i++) {
            nm_inet6_ntop(&ntp_addrs[i], addr_str);
            g_string_append(nm_gstring_add_space_delimiter(str), addr_str);
        }
    }
    if (str->len) {
        nm_dhcp_option_add_option(options,
                                  TRUE,
                                  AF_INET6,
                                  NM_DHCP_OPTION_DHCP6_NTP_SERVER,
                                  str->str);
    }

    nm_l3_config_data_set_dhcp_lease_from_options(l3cd, AF_INET6, g_steal_pointer(&options));

    return g_steal_pointer(&l3cd);
}

static void
bound6_handle(NMDhcpSystemd *self)
{
    NMDhcpSystemdPrivate                   *priv   = NM_DHCP_SYSTEMD_GET_PRIVATE(self);
    const gint32                            ts     = nm_utils_get_monotonic_timestamp_sec();
    nm_auto_unref_l3cd_init NML3ConfigData *l3cd   = NULL;
    gs_free_error GError                   *error  = NULL;
    NMPlatformIP6Address                    prefix = {0};
    sd_dhcp6_lease                         *lease  = NULL;
    guint64                                 lft_valid;
    guint64                                 lft_pref;

    if (sd_dhcp6_client_get_lease(priv->client6, &lease) < 0 || !lease) {
        _LOGW(" no lease!");
        _nm_dhcp_client_notify(NM_DHCP_CLIENT(self), NM_DHCP_CLIENT_EVENT_TYPE_FAIL, NULL);
        return;
    }

    _LOGD("lease available");

    l3cd = lease_to_ip6_config(self, lease, ts, &error);

    if (!l3cd) {
        _LOGW("%s", error->message);
        _nm_dhcp_client_notify(NM_DHCP_CLIENT(self), NM_DHCP_CLIENT_EVENT_TYPE_FAIL, NULL);
        return;
    }

    _nm_dhcp_client_notify(NM_DHCP_CLIENT(self), NM_DHCP_CLIENT_EVENT_TYPE_BOUND, l3cd);

    sd_dhcp6_lease_pd_iterator_reset(lease);
    while (!sd_dhcp6_lease_get_pd_prefix(lease, &prefix.address, &prefix.plen)
           && !sd_dhcp6_lease_get_pd_lifetime(lease, &lft_pref, &lft_valid)) {
        prefix.preferred = lifetime_to_uint32(lft_pref);
        prefix.lifetime  = lifetime_to_uint32(lft_valid);
        prefix.timestamp = ts;
        nm_dhcp_client_emit_ipv6_prefix_delegated(NM_DHCP_CLIENT(self), &prefix);
        sd_dhcp6_lease_pd_iterator_next(lease);
    }
}

static void
dhcp6_event_cb(sd_dhcp6_client *client, int event, gpointer user_data)
{
    NMDhcpSystemd        *self = NM_DHCP_SYSTEMD(user_data);
    NMDhcpSystemdPrivate *priv = NM_DHCP_SYSTEMD_GET_PRIVATE(self);

    nm_assert(priv->client6 == client);

    _LOGD("client event %d", event);

    switch (event) {
    case SD_DHCP6_CLIENT_EVENT_RETRANS_MAX:
        _nm_dhcp_client_notify(NM_DHCP_CLIENT(user_data), NM_DHCP_CLIENT_EVENT_TYPE_TIMEOUT, NULL);
        break;
    case SD_DHCP6_CLIENT_EVENT_RESEND_EXPIRE:
    case SD_DHCP6_CLIENT_EVENT_STOP:
        _nm_dhcp_client_notify(NM_DHCP_CLIENT(user_data), NM_DHCP_CLIENT_EVENT_TYPE_FAIL, NULL);
        break;
    case SD_DHCP6_CLIENT_EVENT_IP_ACQUIRE:
    case SD_DHCP6_CLIENT_EVENT_INFORMATION_REQUEST:
        bound6_handle(self);
        break;
    default:
        _LOGW("unhandled event %d", event);
        break;
    }
}

static gboolean
ip6_start(NMDhcpClient *client, const struct in6_addr *ll_addr, GError **error)
{
    NMDhcpSystemd                                   *self      = NM_DHCP_SYSTEMD(client);
    NMDhcpSystemdPrivate                            *priv      = NM_DHCP_SYSTEMD_GET_PRIVATE(self);
    nm_auto(sd_dhcp6_client_unrefp) sd_dhcp6_client *sd_client = NULL;
    const NMDhcpClientConfig                        *client_config;
    const char                                      *hostname;
    const char                                      *mud_url;
    int                                              r, i;
    const guint8                                    *duid_arr;
    gsize                                            duid_len;
    GBytes                                          *duid;
    gboolean                                         prefix_delegation;

    g_return_val_if_fail(!priv->client6, FALSE);

    client_config = nm_dhcp_client_get_config(client);

    /* TODO: honor nm_dhcp_client_get_anycast_address() */

    duid = client_config->client_id;
    if (!duid || !(duid_arr = g_bytes_get_data(duid, &duid_len)) || duid_len < 2) {
        nm_utils_error_set_literal(error, NM_UTILS_ERROR_UNKNOWN, "missing DUID");
        g_return_val_if_reached(FALSE);
    }

    r = sd_dhcp6_client_new(&sd_client);
    if (r < 0) {
        nm_utils_error_set_errno(error, r, "failed to create dhcp-client: %s");
        return FALSE;
    }

    _LOGT("dhcp-client6: set %p", sd_client);

    sd_dhcp6_client_set_address_request(sd_client, !client_config->v6.info_only);
    sd_dhcp6_client_set_information_request(sd_client,
                                            client_config->v6.info_only
                                                && client_config->v6.needed_prefixes == 0);

    r = sd_dhcp6_client_set_iaid(sd_client, client_config->v6.iaid);
    if (r < 0) {
        nm_utils_error_set_errno(error, r, "failed to set IAID: %s");
        return FALSE;
    }

    r = sd_dhcp6_client_set_duid_raw(sd_client,
                                     unaligned_read_be16(&duid_arr[0]),
                                     &duid_arr[2],
                                     duid_len - 2);
    if (r < 0) {
        nm_utils_error_set_errno(error, r, "failed to set DUID: %s");
        return FALSE;
    }

    r = sd_dhcp6_client_attach_event(sd_client, NULL, 0);
    if (r < 0) {
        nm_utils_error_set_errno(error, r, "failed to attach event: %s");
        return FALSE;
    }

    r = sd_dhcp6_client_set_ifindex(sd_client, nm_dhcp_client_get_ifindex(client));
    if (r < 0) {
        nm_utils_error_set_errno(error, r, "failed to set ifindex: %s");
        return FALSE;
    }

    /* Add requested options */
    for (i = 0; i < (int) G_N_ELEMENTS(_nm_dhcp_option_dhcp6_options); i++) {
        if (_nm_dhcp_option_dhcp6_options[i].include) {
            r = sd_dhcp6_client_set_request_option(sd_client,
                                                   _nm_dhcp_option_dhcp6_options[i].option_num);
            nm_assert(r >= 0 || r == -EEXIST);
        }
    }

    mud_url = client_config->mud_url;
    if (mud_url) {
        r = sd_dhcp6_client_set_request_mud_url(sd_client, mud_url);
        if (r < 0) {
            nm_utils_error_set_errno(error, r, "failed to set mud-url: %s");
            return FALSE;
        }
    }

    prefix_delegation = FALSE;
    if (client_config->v6.needed_prefixes > 0) {
        if (client_config->v6.needed_prefixes > 1) {
            /* FIXME: systemd-networkd API only allows requesting a
             * single prefix */
            _LOGW("dhcp-client6: only one prefix request is supported");
        }
        prefix_delegation = TRUE;
        if (client_config->v6.pd_hint_length > 0) {
            r = sd_dhcp6_client_set_prefix_delegation_hint(sd_client,
                                                           client_config->v6.pd_hint_length,
                                                           &client_config->v6.pd_hint_addr);
            if (r < 0) {
                nm_utils_error_set_errno(error, r, "failed to set prefix delegation hint: %s");
                return FALSE;
            }
        }
    }
    r = sd_dhcp6_client_set_prefix_delegation(sd_client, prefix_delegation);
    if (r < 0) {
        nm_utils_error_set_errno(error, r, "failed to enable prefix delegation: %s");
        return FALSE;
    }

    r = sd_dhcp6_client_set_local_address(sd_client, ll_addr);
    if (r < 0) {
        nm_utils_error_set_errno(error, r, "failed to set local address: %s");
        return FALSE;
    }

    hostname = client_config->hostname;
    r        = sd_dhcp6_client_set_fqdn(sd_client, hostname);
    if (r < 0) {
        nm_utils_error_set_errno(error, r, "failed to set DHCP hostname: %s");
        return FALSE;
    }

    r = sd_dhcp6_client_set_callback(sd_client, dhcp6_event_cb, client);
    if (r < 0) {
        nm_utils_error_set_errno(error, r, "failed to set callback: %s");
        return FALSE;
    }

    priv->client6 = g_steal_pointer(&sd_client);

    r = sd_dhcp6_client_start(priv->client6);
    if (r < 0) {
        sd_dhcp6_client_set_callback(priv->client6, NULL, NULL);
        nm_clear_pointer(&priv->client6, sd_dhcp6_client_unref);
        nm_utils_error_set_errno(error, r, "failed to start client: %s");
        return FALSE;
    }

    nm_dhcp_client_set_effective_client_id(client, duid);

    return TRUE;
}

static void
stop(NMDhcpClient *client, gboolean release)
{
    NMDhcpSystemd        *self = NM_DHCP_SYSTEMD(client);
    NMDhcpSystemdPrivate *priv = NM_DHCP_SYSTEMD_GET_PRIVATE(self);
    int                   r    = 0;

    NM_DHCP_CLIENT_CLASS(nm_dhcp_systemd_parent_class)->stop(client, release);

    _LOGT("dhcp-client6: stop");

    if (!priv->client6)
        return;

    r = sd_dhcp6_client_set_send_release(priv->client6, release);
    if (r)
        _LOGT("dhcp-client6: failed setting send-release");

    sd_dhcp6_client_set_callback(priv->client6, NULL, NULL);
    r = sd_dhcp6_client_stop(priv->client6);
    if (r)
        _LOGW("failed to stop client (%d)", r);
}

/*****************************************************************************/

static void
nm_dhcp_systemd_init(NMDhcpSystemd *self)
{}

static void
dispose(GObject *object)
{
    NMDhcpSystemdPrivate *priv = NM_DHCP_SYSTEMD_GET_PRIVATE(object);

    nm_clear_g_free(&priv->lease_file);

    if (priv->client6) {
        sd_dhcp6_client_stop(priv->client6);
        sd_dhcp6_client_unref(priv->client6);
        priv->client6 = NULL;
    }

    G_OBJECT_CLASS(nm_dhcp_systemd_parent_class)->dispose(object);
}

static void
nm_dhcp_systemd_class_init(NMDhcpSystemdClass *sdhcp_class)
{
    NMDhcpClientClass *client_class = NM_DHCP_CLIENT_CLASS(sdhcp_class);
    GObjectClass      *object_class = G_OBJECT_CLASS(sdhcp_class);

    object_class->dispose = dispose;

    client_class->ip6_start = ip6_start;
    client_class->stop      = stop;
}

const NMDhcpClientFactory _nm_dhcp_client_factory_systemd = {
    .name         = "systemd",
    .get_type_4   = nm_dhcp_nettools_get_type,
    .get_type_6   = nm_dhcp_systemd_get_type,
    .undocumented = TRUE,
};

/*****************************************************************************/

const NMDhcpClientFactory _nm_dhcp_client_factory_internal = {
    .name       = "internal",
    .get_type_4 = nm_dhcp_nettools_get_type,
    .get_type_6 = nm_dhcp_systemd_get_type,
};
