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

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

#include "nm-lndp-ndisc.h"

#include <arpa/inet.h>
#include <netinet/icmp6.h>
#include <stdarg.h>
#include <ndp.h>

#include "NetworkManagerUtils.h"
#include "libnm-glib-aux/nm-str-buf.h"
#include "libnm-platform/nm-platform.h"
#include "libnm-platform/nmp-netns.h"
#include "libnm-systemd-shared/nm-sd-utils-shared.h"
#include "nm-l3cfg.h"
#include "nm-ndisc-private.h"
#include "nm-core-utils.h"

#define _NMLOG_PREFIX_NAME "ndisc-lndp"

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

typedef struct {
    struct ndp *ndp;
    GSource    *event_source;

    struct {
        NMRateLimit pio_lft;
        NMRateLimit mtu;
        NMRateLimit omit_prefix;
        NMRateLimit omit_dns;
        NMRateLimit omit_dnssl;
    } msg_ratelimit;
} NMLndpNDiscPrivate;

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

struct _NMLndpNDisc {
    NMNDisc            parent;
    NMLndpNDiscPrivate _priv;
};

struct _NMLndpNDiscClass {
    NMNDiscClass parent;
};

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

G_DEFINE_TYPE(NMLndpNDisc, nm_lndp_ndisc, NM_TYPE_NDISC)

#define NM_LNDP_NDISC_GET_PRIVATE(self) \
    _NM_GET_PRIVATE(self, NMLndpNDisc, NM_IS_LNDP_NDISC, NMNDisc)

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

/*
 * If we log a message about an invalid RA packet, don't repeat the same message
 * at every packet received or sent. Rate limit the message to 6 every 12 hours
 * per type and per ndisc instance.
 */

#define LOG_INV_RA_WINDOW (12 * 3600)
#define LOG_INV_RA_BURST  6

#define _LOG_INVALID_RA(ndisc, rate_limit, ...)                                  \
    G_STMT_START                                                                 \
    {                                                                            \
        NMNDisc     *__ndisc  = (ndisc);                                         \
        NMRateLimit *__rl     = (rate_limit);                                    \
        const char  *__ifname = nm_ndisc_get_ifname(__ndisc);                    \
                                                                                 \
        if (__ifname && nm_logging_enabled(LOGL_WARN, LOGD_IP6)                  \
            && nm_rate_limit_check(__rl, LOG_INV_RA_WINDOW, LOG_INV_RA_BURST)) { \
            nm_log(LOGL_WARN,                                                    \
                   LOGD_IP6,                                                     \
                   __ifname,                                                     \
                   NULL,                                                         \
                   "ndisc (%s): " _NM_UTILS_MACRO_FIRST(__VA_ARGS__),            \
                   __ifname _NM_UTILS_MACRO_REST(__VA_ARGS__));                  \
        }                                                                        \
    }                                                                            \
    G_STMT_END

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

static gboolean
send_rs(NMNDisc *ndisc, GError **error)
{
    NMLndpNDiscPrivate *priv = NM_LNDP_NDISC_GET_PRIVATE(ndisc);
    struct ndp_msg     *msg;
    int                 errsv;

    errsv = ndp_msg_new(&msg, NDP_MSG_RS);
    if (errsv) {
        g_set_error_literal(error,
                            NM_UTILS_ERROR,
                            NM_UTILS_ERROR_UNKNOWN,
                            "cannot create router solicitation");
        return FALSE;
    }
    ndp_msg_ifindex_set(msg, nm_ndisc_get_ifindex(ndisc));

    errsv = ndp_msg_send(priv->ndp, msg);
    ndp_msg_destroy(msg);
    if (errsv) {
        errsv = nm_errno_native(errsv);
        g_set_error(error,
                    NM_UTILS_ERROR,
                    NM_UTILS_ERROR_UNKNOWN,
                    "%s (%d)",
                    nm_strerror_native(errsv),
                    errsv);
        return FALSE;
    }

    return TRUE;
}

static NMIcmpv6RouterPref
_route_preference_coerce(enum ndp_route_preference pref)
{
#define _ASSERT_ENUM(v1, v2)                                      \
    G_STMT_START                                                  \
    {                                                             \
        G_STATIC_ASSERT((NMIcmpv6RouterPref) (v1) == (v2));       \
        G_STATIC_ASSERT((enum ndp_route_preference)(v2) == (v1)); \
        G_STATIC_ASSERT((gint64) (v1) == (v2));                   \
        G_STATIC_ASSERT((gint64) (v2) == (v1));                   \
    }                                                             \
    G_STMT_END

    switch (pref) {
    case NDP_ROUTE_PREF_LOW:
    case NDP_ROUTE_PREF_MEDIUM:
    case NDP_ROUTE_PREF_HIGH:
        _ASSERT_ENUM(NDP_ROUTE_PREF_LOW, NM_ICMPV6_ROUTER_PREF_LOW);
        _ASSERT_ENUM(NDP_ROUTE_PREF_MEDIUM, NM_ICMPV6_ROUTER_PREF_MEDIUM);
        _ASSERT_ENUM(NDP_ROUTE_PREF_HIGH, NM_ICMPV6_ROUTER_PREF_HIGH);
        return (NMIcmpv6RouterPref) pref;
    }

    /* unexpected value must be treated as MEDIUM (RFC 4191). */
    return NM_ICMPV6_ROUTER_PREF_MEDIUM;
}

static int
receive_ra(struct ndp *ndp, struct ndp_msg *msg, gpointer user_data)
{
    NMNDisc             *ndisc   = (NMNDisc *) user_data;
    NMLndpNDiscPrivate  *priv    = NM_LNDP_NDISC_GET_PRIVATE(ndisc);
    NMNDiscDataInternal *rdata   = ndisc->rdata;
    NMNDiscConfigMap     changed = 0;
    NMNDiscGateway       gateway;
    struct ndp_msgra    *msgra = ndp_msgra(msg);
    struct in6_addr      gateway_addr;
    const gint64         now_msec = nm_utils_get_monotonic_timestamp_msec();
    int                  offset;
    int                  hop_limit;
    guint32              val;

    /* Router discovery is subject to the following RFC documents:
     *
     * http://tools.ietf.org/html/rfc4861
     * http://tools.ietf.org/html/rfc4862
     *
     * The biggest difference from good old DHCP is that all configuration
     * items have their own lifetimes and they are merged from various
     * sources. Router discovery is *not* contract-based, so there is *no*
     * single time when the configuration is finished and updates can
     * come at any time.
     */
    _LOGD("received router advertisement at timestamp %" G_GINT64_FORMAT ".%03d seconds",
          now_msec / 1000,
          (int) (now_msec % 1000));

    gateway_addr = *ndp_msg_addrto(msg);
    if (IN6_IS_ADDR_UNSPECIFIED(&gateway_addr))
        g_return_val_if_reached(0);

    /* DHCP level:
     *
     * The problem with DHCP level is what to do if subsequent
     * router advertisements carry different flags. Currently, we just
     * rewrite the flag with every inbound RA.
     */
    {
        NMNDiscDHCPLevel dhcp_level;

        if (ndp_msgra_flag_managed(msgra))
            dhcp_level = NM_NDISC_DHCP_LEVEL_MANAGED;
        else if (ndp_msgra_flag_other(msgra))
            dhcp_level = NM_NDISC_DHCP_LEVEL_OTHERCONF;
        else
            dhcp_level = NM_NDISC_DHCP_LEVEL_NONE;

        /* when receiving multiple RA (possibly from different routers),
         * let's keep the "most managed" level. */
        G_STATIC_ASSERT_EXPR(NM_NDISC_DHCP_LEVEL_MANAGED > NM_NDISC_DHCP_LEVEL_OTHERCONF);
        G_STATIC_ASSERT_EXPR(NM_NDISC_DHCP_LEVEL_OTHERCONF > NM_NDISC_DHCP_LEVEL_NONE);
        dhcp_level = NM_MAX(dhcp_level, rdata->public.dhcp_level);

        if (dhcp_level != rdata->public.dhcp_level) {
            rdata->public.dhcp_level = dhcp_level;
            changed |= NM_NDISC_CONFIG_DHCP_LEVEL;
        }
    }

    /* Default gateway:
     *
     * Subsequent router advertisements can represent new default gateways
     * on the network. We should present all of them in router preference
     * order.
     *
     * https://tools.ietf.org/html/rfc2461#section-4.2 :
     * A Lifetime of 0 indicates that the router is not a default router and
     * SHOULD NOT appear on the default router list.
     *
     * We handle that by tracking a gateway that expires right now. */
    gateway = (NMNDiscGateway) {
        .address     = gateway_addr,
        .expiry_msec = _nm_ndisc_lifetime_to_expiry(now_msec, ndp_msgra_router_lifetime(msgra)),
        .preference  = _route_preference_coerce(ndp_msgra_route_preference(msgra)),
    };

    /* Addresses & Routes */
    ndp_msg_opt_for_each_offset (offset, msg, NDP_MSG_OPT_PREFIX) {
        guint8          r_plen;
        struct in6_addr r_network;

        /* Device route */

        r_plen = ndp_msg_opt_prefix_len(msg, offset);
        if (r_plen == 0 || r_plen > 128)
            continue;
        nm_ip6_addr_clear_host_address(&r_network, ndp_msg_opt_prefix(msg, offset), r_plen);

        if (IN6_IS_ADDR_UNSPECIFIED(&r_network) || IN6_IS_ADDR_LINKLOCAL(&r_network))
            continue;

        if (ndp_msg_opt_prefix_flag_on_link(msg, offset)) {
            const NMNDiscRoute route = {
                .network = r_network,
                .plen    = r_plen,
                .on_link = TRUE,
                .expiry_msec =
                    _nm_ndisc_lifetime_to_expiry(now_msec,
                                                 ndp_msg_opt_prefix_valid_time(msg, offset)),
            };

            if (nm_ndisc_add_route(ndisc, &route, now_msec))
                changed |= NM_NDISC_CONFIG_ROUTES;
        }

        /* Address */
        if (r_plen == 64 && ndp_msg_opt_prefix_flag_auto_addr_conf(msg, offset)) {
            const guint32  valid_time     = ndp_msg_opt_prefix_valid_time(msg, offset);
            const guint32  preferred_time = ndp_msg_opt_prefix_preferred_time(msg, offset);
            NMNDiscAddress address;

            /*
             * RFC 4862 Section 5.5.3 states:
             * c)  If the preferred lifetime is greater than the valid lifetime,
             * silently ignore the Prefix Information option. A node MAY wish to
             * log a system management error in this case.
             */
            if (preferred_time > valid_time) {
                _LOG_INVALID_RA(
                    ndisc,
                    &priv->msg_ratelimit.pio_lft,
                    "ignoring Prefix Information Option with invalid lifetimes in received IPv6 "
                    "router advertisement");
                continue;
            }

            address = (NMNDiscAddress) {
                .address               = r_network,
                .expiry_msec           = _nm_ndisc_lifetime_to_expiry(now_msec, valid_time),
                .expiry_preferred_msec = _nm_ndisc_lifetime_to_expiry(now_msec, preferred_time),
            };

            if (nm_ndisc_complete_and_add_address(ndisc, &address, now_msec))
                changed |= NM_NDISC_CONFIG_ADDRESSES;
        }
    }
    ndp_msg_opt_for_each_offset (offset, msg, NDP_MSG_OPT_ROUTE) {
        guint8          plen = ndp_msg_opt_route_prefix_len(msg, offset);
        struct in6_addr network;

        if (plen > 128)
            continue;

        if (plen == 0) {
            /* https://tools.ietf.org/html/rfc4191#section-3.1 :
             * When processing a Router Advertisement, a type C host first updates a
             * ::/0 route based on the Router Lifetime and Default Router Preference
             * in the Router Advertisement message header. [...] The Router Preference
             * and Lifetime values in a ::/0 Route Information Option override the
             * preference and lifetime values in the Router Advertisement header.
             */
            gateway.preference =
                _route_preference_coerce(ndp_msg_opt_route_preference(msg, offset));
            gateway.expiry_msec =
                _nm_ndisc_lifetime_to_expiry(now_msec, ndp_msg_opt_route_lifetime(msg, offset));
            continue;
        }

        nm_ip6_addr_clear_host_address(&network, ndp_msg_opt_route_prefix(msg, offset), plen);

        {
            const NMNDiscRoute route = {
                .network = network,
                .gateway = gateway_addr,
                .plen    = plen,
                .on_link = FALSE,
                .expiry_msec =
                    _nm_ndisc_lifetime_to_expiry(now_msec, ndp_msg_opt_route_lifetime(msg, offset)),
                .preference = _route_preference_coerce(ndp_msg_opt_route_preference(msg, offset)),
            };

            /* Routers through this particular gateway */
            if (nm_ndisc_add_route(ndisc, &route, now_msec))
                changed |= NM_NDISC_CONFIG_ROUTES;
        }
    }

    if (nm_ndisc_add_gateway(ndisc, &gateway, now_msec))
        changed |= NM_NDISC_CONFIG_GATEWAYS;

    ndp_msg_opt_for_each_offset (offset, msg, NDP_MSG_OPT_RDNSS) {
        struct in6_addr *addr;
        int              addr_index;

        ndp_msg_opt_rdnss_for_each_addr (addr, addr_index, msg, offset) {
            const NMNDiscDNSServer dns_server = {
                .address = *addr,
                .expiry_msec =
                    _nm_ndisc_lifetime_to_expiry(now_msec, ndp_msg_opt_rdnss_lifetime(msg, offset)),
            };

            if (nm_ndisc_add_dns_server(ndisc, &dns_server, now_msec))
                changed |= NM_NDISC_CONFIG_DNS_SERVERS;
        }
    }
    ndp_msg_opt_for_each_offset (offset, msg, NDP_MSG_OPT_DNSSL) {
        char *domain;
        int   domain_index;

        ndp_msg_opt_dnssl_for_each_domain (domain, domain_index, msg, offset) {
            const NMNDiscDNSDomain dns_domain = {
                .domain = domain,
                .expiry_msec =
                    _nm_ndisc_lifetime_to_expiry(now_msec, ndp_msg_opt_dnssl_lifetime(msg, offset)),
            };

            if (nm_ndisc_add_dns_domain(ndisc, &dns_domain, now_msec))
                changed |= NM_NDISC_CONFIG_DNS_DOMAINS;
        }
    }

    hop_limit = ndp_msgra_curhoplimit(msgra);
    if (rdata->public.hop_limit != hop_limit) {
        rdata->public.hop_limit = hop_limit;
        changed |= NM_NDISC_CONFIG_HOP_LIMIT;
    }

    val = ndp_msgra_reachable_time(msgra);
    if (val && rdata->public.reachable_time_ms != val) {
        rdata->public.reachable_time_ms = val;
        changed |= NM_NDISC_CONFIG_REACHABLE_TIME;
    }

    val = ndp_msgra_retransmit_time(msgra);
    if (val && rdata->public.retrans_timer_ms != val) {
        rdata->public.retrans_timer_ms = val;
        changed |= NM_NDISC_CONFIG_RETRANS_TIMER;
    }

    /* MTU */
    ndp_msg_opt_for_each_offset (offset, msg, NDP_MSG_OPT_MTU) {
        guint32 mtu = ndp_msg_opt_mtu(msg, offset);
        if (mtu >= 1280) {
            if (rdata->public.mtu != mtu) {
                rdata->public.mtu = mtu;
                changed |= NM_NDISC_CONFIG_MTU;
            }
        } else {
            /* All sorts of bad things would happen if we accepted this.
             * Kernel would set it, but would flush out all IPv6 addresses away
             * from the link, even the link-local, and we wouldn't be able to
             * listen for further RAs that could fix the MTU. */
            _LOG_INVALID_RA(ndisc,
                            &priv->msg_ratelimit.mtu,
                            "ignoring too small MTU %u in received IPv6 "
                            "router advertisement",
                            mtu);
        }
    }

    nm_ndisc_ra_received(ndisc, now_msec, changed);
    return 0;
}

static void *
_ndp_msg_add_option(struct ndp_msg *msg, gsize len)
{
    gsize payload_len = ndp_msg_payload_len(msg);
    void *ret         = &((uint8_t *) msg)[payload_len];

    nm_assert(len <= G_MAXSIZE - payload_len);
    len += payload_len;

    if (len > ndp_msg_payload_maxlen(msg))
        return NULL;

    ndp_msg_payload_len_set(msg, len);
    nm_assert(len == ndp_msg_payload_len(msg));
    return ret;
}

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

/* "Recursive DNS Server Option" at https://tools.ietf.org/html/rfc8106#section-5.1 */

#define NM_ND_OPT_RDNSS 25

typedef struct _nm_packed {
    struct nd_opt_hdr header;
    uint16_t          reserved;
    uint32_t          lifetime;
    struct in6_addr   addrs[0];
} NMLndpRdnssOption;

G_STATIC_ASSERT(sizeof(NMLndpRdnssOption) == 8u);

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

/* "DNS Search List Option" at https://tools.ietf.org/html/rfc8106#section-5.2 */

#define NM_ND_OPT_DNSSL 31

typedef struct _nm_packed {
    struct nd_opt_hdr header;
    uint16_t          reserved;
    uint32_t          lifetime;
    uint8_t           search_list[0];
} NMLndpDnsslOption;

G_STATIC_ASSERT(sizeof(NMLndpDnsslOption) == 8u);

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

static gboolean
send_ra(NMNDisc *ndisc, GError **error)
{
    NMLndpNDiscPrivate      *priv  = NM_LNDP_NDISC_GET_PRIVATE(ndisc);
    NMNDiscDataInternal     *rdata = ndisc->rdata;
    int                      errsv;
    struct in6_addr         *addr;
    struct ndp_msg          *msg;
    guint                    i;
    nm_auto_str_buf NMStrBuf sbuf = NM_STR_BUF_INIT(0, FALSE);

    errsv = ndp_msg_new(&msg, NDP_MSG_RA);
    if (errsv) {
        g_set_error_literal(error,
                            NM_UTILS_ERROR,
                            NM_UTILS_ERROR_UNKNOWN,
                            "cannot create a router advertisement");
        return FALSE;
    }

    ndp_msg_ifindex_set(msg, nm_ndisc_get_ifindex(ndisc));

    /* Multicast to all nodes. */
    addr               = ndp_msg_addrto(msg);
    addr->s6_addr32[0] = htonl(0xff020000);
    addr->s6_addr32[1] = 0;
    addr->s6_addr32[2] = 0;
    addr->s6_addr32[3] = htonl(0x1);

    ndp_msgra_router_lifetime_set(ndp_msgra(msg), NM_NDISC_ROUTER_LIFETIME);

    /* The device let us know about all addresses that the device got
     * whose prefixes are suitable for delegating. Let's announce them. */
    for (i = 0; i < rdata->addresses->len; i++) {
        const NMNDiscAddress      *address = &nm_g_array_index(rdata->addresses, NMNDiscAddress, i);
        struct nd_opt_prefix_info *prefix;

        prefix = _ndp_msg_add_option(msg, sizeof(*prefix));
        if (!prefix) {
            /* Maybe we could send separate RAs, but why bother... */
            _LOG_INVALID_RA(
                ndisc,
                &priv->msg_ratelimit.omit_prefix,
                "the outgoing IPv6 router advertisement is too big: omitting some prefixes");
            break;
        }

        prefix->nd_opt_pi_type       = ND_OPT_PREFIX_INFORMATION;
        prefix->nd_opt_pi_len        = 4;
        prefix->nd_opt_pi_prefix_len = 64;
        prefix->nd_opt_pi_flags_reserved |= ND_OPT_PI_FLAG_ONLINK;
        prefix->nd_opt_pi_flags_reserved |= ND_OPT_PI_FLAG_AUTO;
        prefix->nd_opt_pi_valid_time =
            htonl(_nm_ndisc_lifetime_from_expiry(NM_NDISC_EXPIRY_BASE_TIMESTAMP,
                                                 address->expiry_msec,
                                                 TRUE));
        prefix->nd_opt_pi_preferred_time =
            htonl(_nm_ndisc_lifetime_from_expiry(NM_NDISC_EXPIRY_BASE_TIMESTAMP,
                                                 address->expiry_preferred_msec,
                                                 TRUE));
        prefix->nd_opt_pi_prefix.s6_addr32[0] = address->address.s6_addr32[0];
        prefix->nd_opt_pi_prefix.s6_addr32[1] = address->address.s6_addr32[1];
        prefix->nd_opt_pi_prefix.s6_addr32[2] = 0;
        prefix->nd_opt_pi_prefix.s6_addr32[3] = 0;
    }

    if (rdata->dns_servers->len > 0u) {
        NMLndpRdnssOption *option;
        gsize len = sizeof(*option) + (sizeof(option->addrs[0]) * rdata->dns_servers->len);

        option = _ndp_msg_add_option(msg, len);
        if (!option) {
            _LOG_INVALID_RA(
                ndisc,
                &priv->msg_ratelimit.omit_dns,
                "the outgoing IPv6 router advertisement is too big: omitting DNS information");
            goto dns_servers_done;
        }

        option->header.nd_opt_type = NM_ND_OPT_RDNSS;
        option->header.nd_opt_len  = len / 8;
        option->lifetime           = htonl(900);

        for (i = 0; i < rdata->dns_servers->len; i++) {
            const NMNDiscDNSServer *dns_server =
                &nm_g_array_index(rdata->dns_servers, NMNDiscDNSServer, i);

            option->addrs[i] = dns_server->address;
        }
    }
dns_servers_done:

    if (rdata->dns_domains->len > 0u) {
        NMLndpDnsslOption *option;
        gsize              padding;
        gsize              len;

        nm_str_buf_reset(&sbuf);

        for (i = 0; i < rdata->dns_domains->len; i++) {
            const NMNDiscDNSDomain *dns_domain =
                &nm_g_array_index(rdata->dns_domains, NMNDiscDNSDomain, i);
            const char *domain = dns_domain->domain;
            gsize       domain_l;
            gsize       n_reserved;
            int         r;

            if (nm_str_is_empty(domain)) {
                nm_assert_not_reached();
                continue;
            }

            domain_l = strlen(domain);

            nm_str_buf_maybe_expand(&sbuf, domain_l + 2u, FALSE);
            n_reserved = sbuf.allocated - sbuf.len;

            r = nm_sd_dns_name_to_wire_format(
                domain,
                (guint8 *) (&nm_str_buf_get_str_unsafe(&sbuf)[sbuf.len]),
                n_reserved,
                FALSE);

            if (r < 0 || ((gsize) r) > n_reserved) {
                nm_assert(r != -ENOBUFS);
                nm_assert(r < 0);
                /* we don't expect errors here, unless the domain name is invalid.
                 * That should have been caught (and rejected) by upper layers, but
                 * at this point it seems dangerous to assert (as it's hard to review
                 * that all callers got it correct). So instead silently ignore the error. */
                continue;
            }

            nm_str_buf_set_size(&sbuf, sbuf.len + ((gsize) r), TRUE, FALSE);
        }

        if (sbuf.len == 0) {
            /* no valid domains? */
            goto dns_domains_done;
        }

        len     = sizeof(*option) + sbuf.len;
        padding = len % 8u;
        if (padding != 0u) {
            padding = 8u - padding;
            len += padding;
        }

        nm_assert(len % 8u == 0u);
        nm_assert(len > 0u);
        nm_assert(len / 8u >= 2u);

        if (len / 8u >= 256u || !(option = _ndp_msg_add_option(msg, len))) {
            _LOG_INVALID_RA(
                ndisc,
                &priv->msg_ratelimit.omit_dnssl,
                "the outgoing IPv6 router advertisement is too big: omitting DNS search list");
            goto dns_domains_done;
        }

        nm_str_buf_append_c_len(&sbuf, '\0', padding);

        option->header.nd_opt_type = NM_ND_OPT_DNSSL;
        option->header.nd_opt_len  = len / 8u;
        option->reserved           = 0;
        option->lifetime           = htonl(900);
        memcpy(option->search_list, nm_str_buf_get_str_unsafe(&sbuf), sbuf.len);
    }
dns_domains_done:

    errsv = ndp_msg_send(priv->ndp, msg);

    ndp_msg_destroy(msg);
    if (errsv) {
        errsv = nm_errno_native(errsv);
        g_set_error(error,
                    NM_UTILS_ERROR,
                    NM_UTILS_ERROR_UNKNOWN,
                    "%s (%d)",
                    nm_strerror_native(errsv),
                    errsv);
        return FALSE;
    }

    return TRUE;
}

static int
receive_rs(struct ndp *ndp, struct ndp_msg *msg, gpointer user_data)
{
    NMNDisc *ndisc = user_data;

    nm_ndisc_rs_received(ndisc);
    return 0;
}

static gboolean
event_ready(int fd, GIOCondition condition, gpointer user_data)
{
    gs_unref_object NMNDisc    *ndisc = g_object_ref(NM_NDISC(user_data));
    nm_auto_pop_netns NMPNetns *netns = NULL;
    NMLndpNDiscPrivate         *priv  = NM_LNDP_NDISC_GET_PRIVATE(ndisc);

    _LOGD("processing libndp events");

    if (!nm_ndisc_netns_push(ndisc, &netns)) {
        /* something is very wrong. Stop handling events. */
        nm_clear_g_source_inst(&priv->event_source);
        return G_SOURCE_REMOVE;
    }

    ndp_callall_eventfd_handler(priv->ndp);
    return G_SOURCE_CONTINUE;
}

static void
start(NMNDisc *ndisc)
{
    NMLndpNDiscPrivate *priv = NM_LNDP_NDISC_GET_PRIVATE(ndisc);
    int                 fd;

    g_return_if_fail(!priv->event_source);

    fd = ndp_get_eventfd(priv->ndp);

    priv->event_source = nm_g_unix_fd_add_source(fd, G_IO_IN, event_ready, ndisc);

    /* Flush any pending messages to avoid using obsolete information */
    event_ready(fd, 0, ndisc);

    switch (nm_ndisc_get_node_type(ndisc)) {
    case NM_NDISC_NODE_TYPE_HOST:
        ndp_msgrcv_handler_register(priv->ndp,
                                    receive_ra,
                                    NDP_MSG_RA,
                                    nm_ndisc_get_ifindex(ndisc),
                                    ndisc);
        break;
    case NM_NDISC_NODE_TYPE_ROUTER:
        ndp_msgrcv_handler_register(priv->ndp,
                                    receive_rs,
                                    NDP_MSG_RS,
                                    nm_ndisc_get_ifindex(ndisc),
                                    ndisc);
        break;
    default:
        g_assert_not_reached();
    }
}

static void
_cleanup(NMNDisc *ndisc)
{
    NMLndpNDiscPrivate *priv = NM_LNDP_NDISC_GET_PRIVATE(ndisc);

    nm_clear_g_source_inst(&priv->event_source);

    if (priv->ndp) {
        switch (nm_ndisc_get_node_type(ndisc)) {
        case NM_NDISC_NODE_TYPE_HOST:
            ndp_msgrcv_handler_unregister(priv->ndp,
                                          receive_ra,
                                          NDP_MSG_RA,
                                          nm_ndisc_get_ifindex(ndisc),
                                          ndisc);
            break;
        case NM_NDISC_NODE_TYPE_ROUTER:
            ndp_msgrcv_handler_unregister(priv->ndp,
                                          receive_rs,
                                          NDP_MSG_RS,
                                          nm_ndisc_get_ifindex(ndisc),
                                          ndisc);
            break;
        default:
            nm_assert_not_reached();
            break;
        }
        ndp_close(priv->ndp);
        priv->ndp = NULL;
    }
}

static void
stop(NMNDisc *ndisc)
{
    _cleanup(ndisc);
}

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

static void
nm_lndp_ndisc_init(NMLndpNDisc *lndp_ndisc)
{}

NMNDisc *
nm_lndp_ndisc_new(const NMNDiscConfig *config)
{
    nm_auto_pop_netns NMPNetns *netns = NULL;
    gs_unref_object NMNDisc    *ndisc = NULL;
    NMLndpNDiscPrivate         *priv;
    int                         errsv;

    g_return_val_if_fail(config, NULL);
    g_return_val_if_fail(NM_IS_L3CFG(config->l3cfg), NULL);
    g_return_val_if_fail(config->network_id, NULL);

    if (!nm_platform_netns_push(nm_l3cfg_get_platform(config->l3cfg), &netns)) {
        /* The inability to change the name space is also considered
         * a fatal error. We have a FD open to the file descriptor, and
         * it's unclear how to handle (or recover from) a failure to setns(). */
        g_return_val_if_reached(NULL);
    }

    ndisc = g_object_new(NM_TYPE_LNDP_NDISC, NM_NDISC_CONFIG, config, NULL);

    priv = NM_LNDP_NDISC_GET_PRIVATE(ndisc);

    errsv = ndp_open(&priv->ndp);

    if (errsv != 0) {
        /* This is serious. It might be ENOMEM or the inability to open (or modify)
         * a file descriptor. In all cases there is not much reason trying to recover
         * from that. File descriptors are a basic resource, that we just require (just
         * like memory). */
        g_return_val_if_reached(NULL);
    }

    return g_steal_pointer(&ndisc);
}

static void
dispose(GObject *object)
{
    NMNDisc *ndisc = NM_NDISC(object);

    _cleanup(ndisc);

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

static void
nm_lndp_ndisc_class_init(NMLndpNDiscClass *klass)
{
    GObjectClass *object_class = G_OBJECT_CLASS(klass);
    NMNDiscClass *ndisc_class  = NM_NDISC_CLASS(klass);

    object_class->dispose = dispose;

    ndisc_class->start   = start;
    ndisc_class->stop    = stop;
    ndisc_class->send_rs = send_rs;
    ndisc_class->send_ra = send_ra;
}
