/* SPDX-License-Identifier: GPL-2.0-or-later */
/* nm-l2tp-service - l2tp (and other pppd) integration with NetworkManager
 *
 * (C) 2007 - 2008 Novell, Inc.
 * (C) 2008 - 2009 Red Hat, Inc.
 *
 */

#include <config.h>
#define ___CONFIG_H__

#include <string.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <dlfcn.h>

#include "nm-l2tp-pppd-status.h"
#include "nm-l2tp-pppd-compat.h"

#include "nm-default.h"
#include "nm-l2tp-service.h"
#include "nm-utils/nm-shared-utils.h"
#include "nm-utils/nm-vpn-plugin-macros.h"

int plugin_init(void);

char pppd_version[] = PPPD_VERSION;

/*****************************************************************************/
typedef void (*protrej_fn)(int unit);

struct {
    int         log_level;
    const char *log_prefix_token;
    GDBusProxy *proxy;
    bool        has_ip4;
    bool        has_ip6;
    bool        is_ip6_rej;
    protrej_fn  old_protrej;
} gl /*lobal*/;

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

#define _NMLOG(level, ...)                                                                   \
    G_STMT_START                                                                             \
    {                                                                                        \
        if (gl.log_level >= (level)) {                                                       \
            syslog(nm_utils_syslog_coerce_from_nm(level),                                    \
                   "nm-l2tp[%s] %-7s [helper-%ld] " _NM_UTILS_MACRO_FIRST(__VA_ARGS__) "\n", \
                   gl.log_prefix_token,                                                      \
                   nm_utils_syslog_to_str(level),                                            \
                   (long) getpid() _NM_UTILS_MACRO_REST(__VA_ARGS__));                       \
        }                                                                                    \
    }                                                                                        \
    G_STMT_END

#define _LOGI(...) _NMLOG(LOG_NOTICE, __VA_ARGS__)
#define _LOGW(...) _NMLOG(LOG_WARNING, __VA_ARGS__)
#define _LOGE(...) _NMLOG(LOG_ERR, __VA_ARGS__)

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

static void
nm_set_config(gboolean has_ip4, gboolean has_ip6)
{
    GVariantBuilder builder;

    g_return_if_fail(G_IS_DBUS_PROXY(gl.proxy));

    g_variant_builder_init(&builder, G_VARIANT_TYPE_VARDICT);

    g_variant_builder_add(&builder,
                  "{sv}",
                  NM_VPN_PLUGIN_CONFIG_TUNDEV,
                  g_variant_new_string(ppp_ifname()));

    g_variant_builder_add(&builder,
                  "{sv}",
                  NM_VPN_PLUGIN_CONFIG_HAS_IP4,
                  g_variant_new_boolean(has_ip4));

    g_variant_builder_add(&builder,
                  "{sv}",
                  NM_VPN_PLUGIN_CONFIG_HAS_IP6,
                  g_variant_new_boolean(has_ip6));

    _LOGI("config: sending Config to NetworkManager-l2tp (IPv4:%s, IPv6:%s)",
        has_ip4 ? "on" : "off",
        has_ip6 ? "on" : "off");

    g_dbus_proxy_call(gl.proxy,
                "SetConfig",
                g_variant_new("(a{sv})", &builder),
                G_DBUS_CALL_FLAGS_NONE,
                -1,
                NULL,
                NULL,
                NULL);
}

static void nm_ipv6_protrej(int unit);

static void
nm_phasechange(void *data, int arg)
{
    NMPPPStatus ppp_status = NM_PPP_STATUS_UNKNOWN;
    char *      ppp_phase;

    g_return_if_fail(G_IS_DBUS_PROXY(gl.proxy));

    switch (arg) {
    case PHASE_DEAD:
        ppp_status = NM_PPP_STATUS_DEAD;
        ppp_phase  = "dead";
        break;
    case PHASE_INITIALIZE:
        gl.has_ip4    = FALSE;
        gl.has_ip6    = FALSE;
        gl.is_ip6_rej = FALSE;
        ipv6cp_protent.protrej = nm_ipv6_protrej;
        ppp_status = NM_PPP_STATUS_INITIALIZE;
        ppp_phase  = "initialize";
        break;
    case PHASE_SERIALCONN:
        ppp_status = NM_PPP_STATUS_SERIALCONN;
        ppp_phase  = "serial connection";
        break;
    case PHASE_DORMANT:
        ppp_status = NM_PPP_STATUS_DORMANT;
        ppp_phase  = "dormant";
        break;
    case PHASE_ESTABLISH:
        ppp_status = NM_PPP_STATUS_ESTABLISH;
        ppp_phase  = "establish";
        break;
    case PHASE_AUTHENTICATE:
        ppp_status = NM_PPP_STATUS_AUTHENTICATE;
        ppp_phase  = "authenticate";
        break;
    case PHASE_CALLBACK:
        ppp_status = NM_PPP_STATUS_CALLBACK;
        ppp_phase  = "callback";
        break;
    case PHASE_NETWORK:
        ppp_status = NM_PPP_STATUS_NETWORK;
        ppp_phase  = "network";
        break;
    case PHASE_RUNNING:
        ppp_status = NM_PPP_STATUS_RUNNING;
        ppp_phase  = "running";
        break;
    case PHASE_TERMINATE:
        ppp_status = NM_PPP_STATUS_TERMINATE;
        ppp_phase  = "terminate";
        break;
    case PHASE_DISCONNECT:
        ppp_status = NM_PPP_STATUS_DISCONNECT;
        ppp_phase  = "disconnect";
        break;
    case PHASE_HOLDOFF:
        ppp_status = NM_PPP_STATUS_HOLDOFF;
        ppp_phase  = "holdoff";
        break;
    case PHASE_MASTER:
        ppp_status = NM_PPP_STATUS_MASTER;
        ppp_phase  = "master";
        break;

    default:
        ppp_phase = "unknown";
        break;
    }

    _LOGI("phasechange: status %d / phase '%s'", ppp_status, ppp_phase);

    if (ppp_status != NM_PPP_STATUS_UNKNOWN) {
        g_dbus_proxy_call(gl.proxy,
                          "SetState",
                          g_variant_new("(u)", ppp_status),
                          G_DBUS_CALL_FLAGS_NONE,
                          -1,
                          NULL,
                          NULL,
                          NULL);
    }
}

static void
nm_ip_up(void *data, int arg)
{
    guint32         pppd_made_up_address = htonl (0x0a404040 + ppp_ifunit());
    ipcp_options    opts                 = ipcp_gotoptions[0];
    ipcp_options    peer_opts            = ipcp_hisoptions[0];
    GVariantBuilder builder;

    g_return_if_fail(G_IS_DBUS_PROXY(gl.proxy));

    _LOGI("ip-up: event");

    if (!opts.ouraddr) {
        _LOGW("ip-up: didn't receive an internal IP from pppd!");
        nm_phasechange(NULL, PHASE_DEAD);
        return;
    }

    gl.has_ip4 = TRUE;
    nm_set_config(gl.has_ip4, gl.has_ip6);

    g_variant_builder_init(&builder, G_VARIANT_TYPE_VARDICT);

    g_variant_builder_add(&builder,
                          "{sv}",
                          NM_VPN_PLUGIN_IP4_CONFIG_TUNDEV,
                          g_variant_new_string(ppp_ifname()));

    g_variant_builder_add(&builder,
                          "{sv}",
                          NM_VPN_PLUGIN_IP4_CONFIG_ADDRESS,
                          g_variant_new_uint32(opts.ouraddr));

    /* Prefer the peer options remote address first, _unless_ pppd made the
     * address up, at which point prefer the local options remote address,
     * and if that's not right, use the made-up address as a last resort.
     */
    if (peer_opts.hisaddr && (peer_opts.hisaddr != pppd_made_up_address)) {
        g_variant_builder_add(&builder,
                              "{sv}",
                              NM_VPN_PLUGIN_IP4_CONFIG_PTP,
                              g_variant_new_uint32(peer_opts.hisaddr));
    } else if (opts.hisaddr){
        g_variant_builder_add(&builder,
                              "{sv}",
                              NM_VPN_PLUGIN_IP4_CONFIG_PTP,
                              g_variant_new_uint32(opts.hisaddr));
    } else if (peer_opts.hisaddr == pppd_made_up_address) {
        /* As a last resort, use the made-up address */
        g_variant_builder_add(&builder,
                              "{sv}",
                              NM_VPN_PLUGIN_IP4_CONFIG_PTP,
                              g_variant_new_uint32(pppd_made_up_address));
    }

    g_variant_builder_add(&builder,
                          "{sv}",
                          NM_VPN_PLUGIN_IP4_CONFIG_PREFIX,
                          g_variant_new_uint32(32));

    if (opts.dnsaddr[0] || opts.dnsaddr[1]) {
        guint32 dns[2];
        int     len = 0;

        if (opts.dnsaddr[0])
            dns[len++] = opts.dnsaddr[0];
        if (opts.dnsaddr[1])
            dns[len++] = opts.dnsaddr[1];

        g_variant_builder_add(
            &builder,
            "{sv}",
            NM_VPN_PLUGIN_IP4_CONFIG_DNS,
            g_variant_new_fixed_array(G_VARIANT_TYPE_UINT32, dns, len, sizeof(guint32)));
    }

    _LOGI("ip-up: sending Ip4Config to NetworkManager-l2tp...");

    g_dbus_proxy_call(gl.proxy,
                      "SetIp4Config",
                      g_variant_new("(a{sv})", &builder),
                      G_DBUS_CALL_FLAGS_NONE,
                      -1,
                      NULL,
                      NULL,
                      NULL);
}
static void
nm_ip6_up(void *data, int arg)
{
    ipv6cp_options  opts      = ipv6cp_gotoptions[0];
    ipv6cp_options  peer_opts = ipv6cp_hisoptions[0];
    GVariantBuilder builder;
    guchar          addr[16];
    guchar          peer_addr[16];

    g_return_if_fail(G_IS_DBUS_PROXY(gl.proxy));

    _LOGI("ip6-up: event");

    if (gl.is_ip6_rej || eui64_iszero(opts.ourid) || eui64_iszero(peer_opts.hisid)) {
        _LOGI("ip6-up: no IPv6 addresses negotiated");
        return;
    }

    if (eui64_equals(opts.ourid, peer_opts.hisid)) {
        _LOGI("ip6-up: local and remote addresses are equal");
        return;
    }

    /* Build link-local addresses from negotiated interface identifiers.
     * Format: fe80::/64 + 64-bit interface identifier (ourid/hisid).
     */
    memset(addr, 0, sizeof(addr));
    addr[0] = 0xfe;
    addr[1] = 0x80;
    memcpy(&addr[8], opts.ourid.e8, 8);

    memset(peer_addr, 0, sizeof(peer_addr));
    peer_addr[0] = 0xfe;
    peer_addr[1] = 0x80;
    memcpy(&peer_addr[8], peer_opts.hisid.e8, 8);

    gl.has_ip6 = TRUE;
    nm_set_config(gl.has_ip4, gl.has_ip6);

    g_variant_builder_init(&builder, G_VARIANT_TYPE_VARDICT);

    g_variant_builder_add(&builder,
                          "{sv}",
                          NM_VPN_PLUGIN_CONFIG_TUNDEV,
                          g_variant_new_string(ppp_ifname()));

    g_variant_builder_add(&builder,
                          "{sv}",
                          NM_VPN_PLUGIN_IP6_CONFIG_ADDRESS,
                          g_variant_new_fixed_array(G_VARIANT_TYPE_BYTE,
                                                    addr, 16, 1));

    g_variant_builder_add(&builder,
                          "{sv}",
                          NM_VPN_PLUGIN_IP6_CONFIG_PTP,
                          g_variant_new_fixed_array(G_VARIANT_TYPE_BYTE,
                                                    peer_addr, 16, 1));

    g_variant_builder_add(&builder,
                          "{sv}",
                          NM_VPN_PLUGIN_IP6_CONFIG_PREFIX,
                          g_variant_new_uint32(128));

    _LOGI("ip6-up: sending Ip6Config to NetworkManager-l2tp...");

    g_dbus_proxy_call(gl.proxy,
                      "SetIp6Config",
                      g_variant_new("(a{sv})", &builder),
                      G_DBUS_CALL_FLAGS_NONE,
                      -1,
                      NULL,
                      NULL,
                      NULL);
}

static void
nm_ip6_rejected(void)
{
    GVariantBuilder builder;

    g_return_if_fail(G_IS_DBUS_PROXY(gl.proxy));

    gl.has_ip6 = FALSE;
    nm_set_config(gl.has_ip4, gl.has_ip6);

    g_variant_builder_init(&builder, G_VARIANT_TYPE_VARDICT);
    g_variant_builder_add(&builder,
                          "{sv}",
                          NM_VPN_PLUGIN_CONFIG_TUNDEV,
                          g_variant_new_string(ppp_ifname()));

    _LOGI("ip6-protrej: sending empty Ip6Config to NetworkManager-l2tp");

    g_dbus_proxy_call(gl.proxy,
                      "SetIp6Config",
                      g_variant_new("(a{sv})", &builder),
                      G_DBUS_CALL_FLAGS_NONE,
                      -1,
                      NULL,
                      NULL,
                      NULL);
}

#if WITH_PPP_VERSION < PPP_VERSION(2,5,0)
static void
nm_ip6_up_hook(void)
{
    nm_ip6_up(NULL, 0);
}
#endif

/*
 * If peer rejected the IPv6CP protocol, then we won't get an ip6_up callback.
 */
static void
nm_ipv6_protrej(int unit)
{
    gl.is_ip6_rej = TRUE;
    nm_ip6_rejected();
    if (gl.old_protrej)
        (*gl.old_protrej)(unit);
    ipv6cp_protent.protrej = gl.old_protrej;
}

static int
get_chap_check(void)
{
    return 1;
}

static int
get_pap_check(void)
{
    return 1;
}

static int
get_credentials(char *username, char *password)
{
    const char *my_username = NULL;
    const char *my_password = NULL;
    GVariant *  ret;
    GError *    error = NULL;

    if (!password) {
        /* pppd is checking pap support; return 1 for supported */
        g_return_val_if_fail(username, -1);
        return 1;
    }

    g_return_val_if_fail(username, -1);
    g_return_val_if_fail(G_IS_DBUS_PROXY(gl.proxy), -1);

    _LOGI("passwd-hook: requesting credentials...");

    ret = g_dbus_proxy_call_sync(gl.proxy,
                                 "NeedSecrets",
                                 NULL,
                                 G_DBUS_CALL_FLAGS_NONE,
                                 -1,
                                 NULL,
                                 &error);
    if (!ret) {
        _LOGW("passwd-hook: could not get secrets: %s", error->message);
        g_error_free(error);
        return -1;
    }

    _LOGI("passwd-hook: got credentials from NetworkManager-l2tp");

    g_variant_get(ret, "(&s&s)", &my_username, &my_password);

    if (my_username)
        g_strlcpy(username, my_username, MAXNAMELEN);

    if (my_password) {
        g_strlcpy(password, my_password, MAXSECRETLEN);
    }

    g_variant_unref(ret);

    return 1;
}

static void
nm_exit_notify(void *data, int arg)
{
    g_return_if_fail(G_IS_DBUS_PROXY(gl.proxy));

    _LOGI("exit: cleaning up");
    g_clear_object(&gl.proxy);
}

int
plugin_init(void)
{
    GDBusConnection *bus;
    GError *         error = NULL;
    const char *     bus_name;

    g_return_val_if_fail(!gl.proxy, -1);

    bus_name = getenv("NM_DBUS_SERVICE_L2TP");
    if (!bus_name)
        bus_name = NM_DBUS_SERVICE_L2TP;

    gl.log_level =
        _nm_utils_ascii_str_to_int64(getenv("NM_VPN_LOG_LEVEL"), 10, 0, LOG_DEBUG, LOG_NOTICE);
    gl.log_prefix_token = getenv("NM_VPN_LOG_PREFIX_TOKEN") ?: "???";

    _LOGI("initializing");

    bus = g_bus_get_sync(G_BUS_TYPE_SYSTEM, NULL, &error);
    if (!bus) {
        _LOGE("couldn't connect to system bus: %s", error->message);
        g_error_free(error);
        return -1;
    }

    gl.proxy = g_dbus_proxy_new_sync(bus,
                                     G_DBUS_PROXY_FLAGS_DO_NOT_LOAD_PROPERTIES,
                                     NULL,
                                     bus_name,
                                     NM_DBUS_PATH_L2TP_PPP,
                                     NM_DBUS_INTERFACE_L2TP_PPP,
                                     NULL,
                                     &error);
    g_object_unref(bus);

    if (!gl.proxy) {
        _LOGE("couldn't create D-Bus proxy: %s", error->message);
        g_error_free(error);
        return -1;
    }

    chap_passwd_hook = get_credentials;
    chap_check_hook  = get_chap_check;
    pap_passwd_hook  = get_credentials;
    pap_check_hook   = get_pap_check;
#if defined(USE_EAPTLS) || defined(PPP_WITH_EAPTLS)
    eaptls_passwd_hook = get_credentials;
#endif

#if WITH_PPP_VERSION < PPP_VERSION(2,5,0)
    add_notifier(&phasechange, nm_phasechange, NULL);
    add_notifier(&ip_up_notifier, nm_ip_up, NULL);
    ipv6_up_hook = nm_ip6_up_hook;
    add_notifier(&exitnotify, nm_exit_notify, NULL);
#else
    ppp_add_notify(NF_PHASE_CHANGE, nm_phasechange, NULL);
    ppp_add_notify(NF_IP_UP, nm_ip_up, NULL);
    ppp_add_notify(NF_IPV6_UP, nm_ip6_up, NULL);
    ppp_add_notify(NF_EXIT, nm_exit_notify, NULL);
#endif

    gl.old_protrej = ipv6cp_protent.protrej;
    ipv6cp_protent.protrej = nm_ipv6_protrej;

    return 0;
}
