Blob Blame History Raw
/* SPDX-License-Identifier: GPL-2.0-or-later */
/*
 * Copyright (C) 2014 Red Hat, Inc.
 */

#include "nm-default.h"

#include <glib-unix.h>
#include <getopt.h>
#include <locale.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/resource.h>
#include <sys/stat.h>
#include <signal.h>
#include <linux/rtnetlink.h>

#include "nm-glib-aux/nm-c-list.h"

#include "main-utils.h"
#include "NetworkManagerUtils.h"
#include "platform/nm-linux-platform.h"
#include "platform/nm-platform-utils.h"
#include "dhcp/nm-dhcp-manager.h"
#include "ndisc/nm-ndisc.h"
#include "ndisc/nm-lndp-ndisc.h"
#include "nm-utils.h"
#include "nm-core-internal.h"
#include "nm-setting-ip6-config.h"
#include "systemd/nm-sd.h"

#if !defined(NM_DIST_VERSION)
    #define NM_DIST_VERSION VERSION
#endif

#define NMIH_PID_FILE_FMT NMRUNDIR "/nm-iface-helper-%d.pid"

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

static struct {
    GMainLoop *main_loop;
    int        ifindex;
    gboolean   is_vrf_device;

    guint dad_failed_id;
    CList dad_failed_lst_head;
} gl /*obal*/ = {
    .ifindex       = -1,
    .is_vrf_device = FALSE,
};

static struct {
    gboolean                      slaac;
    gboolean                      show_version;
    gboolean                      become_daemon;
    gboolean                      debug;
    gboolean                      g_fatal_warnings;
    gboolean                      slaac_required;
    gboolean                      dhcp4_required;
    int                           tempaddr;
    char *                        ifname;
    char *                        uuid;
    char *                        stable_id;
    char *                        dhcp4_address;
    char *                        dhcp4_clientid;
    char *                        dhcp4_hostname;
    char *                        dhcp4_fqdn;
    char *                        mud_url;
    char *                        iid_str;
    NMSettingIP6ConfigAddrGenMode addr_gen_mode;
    char *                        logging_backend;
    char *                        opt_log_level;
    char *                        opt_log_domains;
    guint32                       priority_v4;
    guint32                       priority_v6;
} global_opt = {
    .tempaddr    = NM_SETTING_IP6_CONFIG_PRIVACY_UNKNOWN,
    .priority_v4 = NM_PLATFORM_ROUTE_METRIC_DEFAULT_IP4,
    .priority_v6 = NM_PLATFORM_ROUTE_METRIC_DEFAULT_IP6,
};

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

#define _NMLOG_PREFIX_NAME "nm-iface-helper"
#define _NMLOG(level, domain, ...) \
    nm_log((level),                \
           (domain),               \
           global_opt.ifname,      \
           NULL,                   \
           "iface-helper: " _NM_UTILS_MACRO_FIRST(__VA_ARGS__) _NM_UTILS_MACRO_REST(__VA_ARGS__))

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

static void
dhcp4_state_changed(NMDhcpClient *client,
                    NMDhcpState   state,
                    NMIP4Config * ip4_config,
                    GHashTable *  options,
                    gpointer      user_data)
{
    static NMIP4Config *last_config = NULL;
    NMIP4Config *       existing;
    gs_unref_ptrarray GPtrArray *ip4_dev_route_blacklist = NULL;
    gs_free_error GError *error                          = NULL;

    g_return_if_fail(!ip4_config || NM_IS_IP4_CONFIG(ip4_config));

    _LOGD(LOGD_DHCP4, "new DHCPv4 client state %d", state);

    switch (state) {
    case NM_DHCP_STATE_BOUND:
    case NM_DHCP_STATE_EXTENDED:
        g_assert(ip4_config);
        g_assert(nm_ip4_config_get_ifindex(ip4_config) == gl.ifindex);

        existing = nm_ip4_config_capture(nm_platform_get_multi_idx(NM_PLATFORM_GET),
                                         NM_PLATFORM_GET,
                                         gl.ifindex);
        if (last_config)
            nm_ip4_config_subtract(existing, last_config, 0);

        nm_ip4_config_merge(existing, ip4_config, NM_IP_CONFIG_MERGE_DEFAULT, 0);
        nm_ip4_config_add_dependent_routes(existing,
                                           RT_TABLE_MAIN,
                                           global_opt.priority_v4,
                                           gl.is_vrf_device,
                                           &ip4_dev_route_blacklist);
        if (!nm_ip4_config_commit(existing, NM_PLATFORM_GET, NM_IP_ROUTE_TABLE_SYNC_MODE_MAIN))
            _LOGW(LOGD_DHCP4, "failed to apply DHCPv4 config");

        if (!last_config && !nm_dhcp_client_accept(client, &error))
            _LOGW(LOGD_DHCP4, "failed to accept lease: %s", error->message);

        nm_platform_ip4_dev_route_blacklist_set(NM_PLATFORM_GET,
                                                gl.ifindex,
                                                ip4_dev_route_blacklist);

        if (last_config)
            g_object_unref(last_config);
        last_config = nm_ip4_config_new(nm_platform_get_multi_idx(NM_PLATFORM_GET),
                                        nm_dhcp_client_get_ifindex(client));
        nm_ip4_config_replace(last_config, ip4_config, NULL);
        break;
    case NM_DHCP_STATE_TIMEOUT:
    case NM_DHCP_STATE_DONE:
    case NM_DHCP_STATE_FAIL:
        if (global_opt.dhcp4_required) {
            _LOGW(LOGD_DHCP4, "DHCPv4 timed out or failed, quitting...");
            g_main_loop_quit(gl.main_loop);
        } else
            _LOGW(LOGD_DHCP4, "DHCPv4 timed out or failed");
        break;
    default:
        break;
    }
}

static void
ndisc_config_changed(NMNDisc *          ndisc,
                     const NMNDiscData *rdata,
                     guint              changed_int,
                     gpointer           user_data)
{
    NMNDiscConfigMap    changed      = changed_int;
    static NMIP6Config *ndisc_config = NULL;
    NMIP6Config *       existing;

    existing = nm_ip6_config_capture(nm_platform_get_multi_idx(NM_PLATFORM_GET),
                                     NM_PLATFORM_GET,
                                     gl.ifindex,
                                     global_opt.tempaddr);
    if (ndisc_config)
        nm_ip6_config_subtract(existing, ndisc_config, 0);
    else {
        ndisc_config = nm_ip6_config_new(nm_platform_get_multi_idx(NM_PLATFORM_GET), gl.ifindex);
    }

    if (changed & NM_NDISC_CONFIG_ADDRESSES) {
        guint8  plen;
        guint32 ifa_flags;

        /* Check, whether kernel is recent enough to help user space handling RA.
         * If it's not supported, we have no ipv6-privacy and must add autoconf
         * addresses as /128. The reason for the /128 is to prevent the kernel
         * from adding a prefix route for this address. */
        ifa_flags = 0;
        if (nm_platform_kernel_support_get(NM_PLATFORM_KERNEL_SUPPORT_TYPE_EXTENDED_IFA_FLAGS)) {
            ifa_flags |= IFA_F_NOPREFIXROUTE;
            if (NM_IN_SET(global_opt.tempaddr,
                          NM_SETTING_IP6_CONFIG_PRIVACY_PREFER_TEMP_ADDR,
                          NM_SETTING_IP6_CONFIG_PRIVACY_PREFER_PUBLIC_ADDR))
                ifa_flags |= IFA_F_MANAGETEMPADDR;
            plen = 64;
        } else
            plen = 128;

        nm_ip6_config_reset_addresses_ndisc(ndisc_config,
                                            rdata->addresses,
                                            rdata->addresses_n,
                                            plen,
                                            ifa_flags);
    }

    if (NM_FLAGS_ANY(changed, NM_NDISC_CONFIG_ROUTES | NM_NDISC_CONFIG_GATEWAYS)) {
        nm_ip6_config_reset_routes_ndisc(
            ndisc_config,
            rdata->gateways,
            rdata->gateways_n,
            rdata->routes,
            rdata->routes_n,
            RT_TABLE_MAIN,
            global_opt.priority_v6,
            nm_platform_kernel_support_get(NM_PLATFORM_KERNEL_SUPPORT_TYPE_RTA_PREF));
    }

    if (changed & NM_NDISC_CONFIG_DHCP_LEVEL) {
        /* Unsupported until systemd DHCPv6 is ready */
    }

    if (changed & NM_NDISC_CONFIG_HOP_LIMIT)
        nm_platform_sysctl_ip_conf_set_ipv6_hop_limit_safe(NM_PLATFORM_GET,
                                                           global_opt.ifname,
                                                           rdata->hop_limit);

    if (changed & NM_NDISC_CONFIG_REACHABLE_TIME) {
        nm_platform_sysctl_ip_neigh_set_ipv6_reachable_time(NM_PLATFORM_GET,
                                                            global_opt.ifname,
                                                            rdata->reachable_time_ms);
    }

    if (changed & NM_NDISC_CONFIG_RETRANS_TIMER) {
        nm_platform_sysctl_ip_neigh_set_ipv6_retrans_time(NM_PLATFORM_GET,
                                                          global_opt.ifname,
                                                          rdata->retrans_timer_ms);
    }

    if (changed & NM_NDISC_CONFIG_MTU) {
        nm_platform_sysctl_ip_conf_set_int64(NM_PLATFORM_GET,
                                             AF_INET6,
                                             global_opt.ifname,
                                             "mtu",
                                             rdata->mtu);
    }

    nm_ip6_config_merge(existing, ndisc_config, NM_IP_CONFIG_MERGE_DEFAULT, 0);
    nm_ip6_config_add_dependent_routes(existing,
                                       RT_TABLE_MAIN,
                                       global_opt.priority_v6,
                                       gl.is_vrf_device);
    if (!nm_ip6_config_commit(existing, NM_PLATFORM_GET, NM_IP_ROUTE_TABLE_SYNC_MODE_MAIN, NULL))
        _LOGW(LOGD_IP6, "failed to apply IPv6 config");
}

static void
ndisc_ra_timeout(NMNDisc *ndisc, gpointer user_data)
{
    if (global_opt.slaac_required) {
        _LOGW(LOGD_IP6, "IPv6 timed out or failed, quitting...");
        g_main_loop_quit(gl.main_loop);
    } else
        _LOGW(LOGD_IP6, "IPv6 timed out or failed");
}

static gboolean
quit_handler(gpointer user_data)
{
    g_main_loop_quit(gl.main_loop);
    return G_SOURCE_REMOVE;
}

static void
setup_signals(void)
{
    signal(SIGPIPE, SIG_IGN);
    g_unix_signal_add(SIGINT, quit_handler, NULL);
    g_unix_signal_add(SIGTERM, quit_handler, NULL);
}

static gboolean
do_early_setup(int *argc, char **argv[])
{
    gint64       priority64_v4 = -1;
    gint64       priority64_v6 = -1;
    GOptionEntry options[]     = {
        /* Interface/IP config */
        {"ifname",
         'i',
         0,
         G_OPTION_ARG_STRING,
         &global_opt.ifname,
         N_("The interface to manage"),
         "eth0"},
        {"uuid",
         'u',
         0,
         G_OPTION_ARG_STRING,
         &global_opt.uuid,
         N_("Connection UUID"),
         "661e8cd0-b618-46b8-9dc9-31a52baaa16b"},
        {"stable-id",
         '\0',
         0,
         G_OPTION_ARG_STRING,
         &global_opt.stable_id,
         N_("Connection Token for Stable IDs"),
         "eth"},
        {"slaac",
         's',
         0,
         G_OPTION_ARG_NONE,
         &global_opt.slaac,
         N_("Whether to manage IPv6 SLAAC"),
         NULL},
        {"slaac-required",
         '6',
         0,
         G_OPTION_ARG_NONE,
         &global_opt.slaac_required,
         N_("Whether SLAAC must be successful"),
         NULL},
        {"slaac-tempaddr",
         't',
         0,
         G_OPTION_ARG_INT,
         &global_opt.tempaddr,
         N_("Use an IPv6 temporary privacy address"),
         NULL},
        {"dhcp4",
         'd',
         0,
         G_OPTION_ARG_STRING,
         &global_opt.dhcp4_address,
         N_("Current DHCPv4 address"),
         NULL},
        {"dhcp4-required",
         '4',
         0,
         G_OPTION_ARG_NONE,
         &global_opt.dhcp4_required,
         N_("Whether DHCPv4 must be successful"),
         NULL},
        {"dhcp4-clientid",
         'c',
         0,
         G_OPTION_ARG_STRING,
         &global_opt.dhcp4_clientid,
         N_("Hex-encoded DHCPv4 client ID"),
         NULL},
        {"dhcp4-hostname",
         'h',
         0,
         G_OPTION_ARG_STRING,
         &global_opt.dhcp4_hostname,
         N_("Hostname to send to DHCP server"),
         N_("barbar")},
        {"dhcp4-fqdn",
         'F',
         0,
         G_OPTION_ARG_STRING,
         &global_opt.dhcp4_fqdn,
         N_("FQDN to send to DHCP server"),
         N_("host.domain.org")},
        {"priority4",
         '\0',
         0,
         G_OPTION_ARG_INT64,
         &priority64_v4,
         N_("Route priority for IPv4"),
         N_("0")},
        {"priority6",
         '\0',
         0,
         G_OPTION_ARG_INT64,
         &priority64_v6,
         N_("Route priority for IPv6"),
         N_("1024")},
        {"iid",
         'e',
         0,
         G_OPTION_ARG_STRING,
         &global_opt.iid_str,
         N_("Hex-encoded Interface Identifier"),
         ""},
        {"addr-gen-mode",
         'e',
         0,
         G_OPTION_ARG_INT,
         &global_opt.addr_gen_mode,
         N_("IPv6 SLAAC address generation mode"),
         "eui64"},
        {"logging-backend",
         '\0',
         0,
         G_OPTION_ARG_STRING,
         &global_opt.logging_backend,
         N_("The logging backend configuration value. See logging.backend in NetworkManager.conf"),
         NULL},

        /* Logging/debugging */
        {"version",
         'V',
         0,
         G_OPTION_ARG_NONE,
         &global_opt.show_version,
         N_("Print NetworkManager version and exit"),
         NULL},
        {"no-daemon",
         'n',
         G_OPTION_FLAG_REVERSE,
         G_OPTION_ARG_NONE,
         &global_opt.become_daemon,
         N_("Don't become a daemon"),
         NULL},
        {"debug",
         'b',
         0,
         G_OPTION_ARG_NONE,
         &global_opt.debug,
         N_("Don't become a daemon, and log to stderr"),
         NULL},
        {"log-level",
         0,
         0,
         G_OPTION_ARG_STRING,
         &global_opt.opt_log_level,
         N_("Log level: one of [%s]"),
         "INFO"},
        {"log-domains",
         0,
         0,
         G_OPTION_ARG_STRING,
         &global_opt.opt_log_domains,
         N_("Log domains separated by ',': any combination of [%s]"),
         "PLATFORM,RFKILL,WIFI"},
        {"g-fatal-warnings",
         0,
         0,
         G_OPTION_ARG_NONE,
         &global_opt.g_fatal_warnings,
         N_("Make all warnings fatal"),
         NULL},
        {NULL}};

    if (!nm_main_utils_early_setup("nm-iface-helper",
                                   argc,
                                   argv,
                                   options,
                                   NULL,
                                   NULL,
                                   _("nm-iface-helper is a small, standalone process that manages "
                                     "a single network interface.")))
        return FALSE;

    if (priority64_v4 >= 0 && priority64_v4 <= G_MAXUINT32)
        global_opt.priority_v4 = (guint32) priority64_v4;
    if (priority64_v6 >= 0 && priority64_v6 <= G_MAXUINT32)
        global_opt.priority_v6 = (guint32) priority64_v6;
    return TRUE;
}

typedef struct {
    NMPlatform *platform;
    NMNDisc *   ndisc;
} DadFailedHandleData;

static gboolean
dad_failed_handle_idle(gpointer user_data)
{
    DadFailedHandleData *data = user_data;
    NMCListElem *        elem;

    while ((elem = c_list_first_entry(&gl.dad_failed_lst_head, NMCListElem, lst))) {
        nm_auto_nmpobj const NMPObject *obj = elem->data;

        nm_c_list_elem_free(elem);

        if (nm_ndisc_dad_addr_is_fail_candidate(data->platform, obj)) {
            nm_ndisc_dad_failed(data->ndisc, &NMP_OBJECT_CAST_IP6_ADDRESS(obj)->address, TRUE);
        }
    }

    gl.dad_failed_id = 0;
    return G_SOURCE_REMOVE;
}

static void
ip6_address_changed(NMPlatform *                platform,
                    int                         obj_type_i,
                    int                         iface,
                    const NMPlatformIP6Address *addr,
                    int                         change_type_i,
                    NMNDisc *                   ndisc)
{
    const NMPlatformSignalChangeType change_type = change_type_i;
    DadFailedHandleData *            data;

    if (!nm_ndisc_dad_addr_is_fail_candidate_event(change_type, addr))
        return;

    c_list_link_tail(
        &gl.dad_failed_lst_head,
        &nm_c_list_elem_new_stale((gpointer) nmp_object_ref(NMP_OBJECT_UP_CAST(addr)))->lst);
    if (gl.dad_failed_id)
        return;

    data             = g_slice_new(DadFailedHandleData);
    data->platform   = platform;
    data->ndisc      = ndisc;
    gl.dad_failed_id = g_idle_add_full(G_PRIORITY_DEFAULT_IDLE,
                                       dad_failed_handle_idle,
                                       data,
                                       nm_g_slice_free_fcn(DadFailedHandleData));
}

int
main(int argc, char *argv[])
{
    char *        bad_domains                  = NULL;
    gs_free_error GError *error                = NULL;
    gboolean              wrote_pidfile        = FALSE;
    gs_free char *        pidfile              = NULL;
    gs_unref_object NMDhcpClient *dhcp4_client = NULL;
    gs_unref_object NMNDisc *ndisc             = NULL;
    gs_unref_bytes GBytes *hwaddr              = NULL;
    gs_unref_bytes GBytes *bcast_hwaddr        = NULL;
    gs_unref_bytes GBytes *client_id           = NULL;
    gs_free NMUtilsIPv6IfaceId *iid            = NULL;
    const NMPlatformLink *      pllink;
    guint                       sd_id;
    int                         errsv;

    c_list_init(&gl.dad_failed_lst_head);

    setpgid(getpid(), getpid());

    if (!do_early_setup(&argc, &argv))
        return 1;

    nm_logging_init_pre("nm-iface-helper",
                        g_strdup_printf("%s[%ld] (%s): ",
                                        _NMLOG_PREFIX_NAME,
                                        (long) getpid(),
                                        global_opt.ifname ?: "???"));

    if (global_opt.g_fatal_warnings) {
        GLogLevelFlags fatal_mask;

        fatal_mask = g_log_set_always_fatal(G_LOG_FATAL_MASK);
        fatal_mask |= G_LOG_LEVEL_WARNING | G_LOG_LEVEL_CRITICAL;
        g_log_set_always_fatal(fatal_mask);
    }

    if (global_opt.show_version) {
        fprintf(stdout, NM_DIST_VERSION "\n");
        return 0;
    }

    nm_main_utils_ensure_root();

    if (!global_opt.ifname || !global_opt.uuid) {
        fprintf(stderr, _("An interface name and UUID are required\n"));
        return 1;
    }

    gl.ifindex = nmp_utils_if_nametoindex(global_opt.ifname);
    if (gl.ifindex <= 0) {
        errsv = errno;
        fprintf(stderr,
                _("Failed to find interface index for %s (%s)\n"),
                global_opt.ifname,
                nm_strerror_native(errsv));
        return 1;
    }
    pidfile = g_strdup_printf(NMIH_PID_FILE_FMT, gl.ifindex);
    nm_main_utils_ensure_not_running_pidfile(pidfile);

    nm_main_utils_ensure_rundir();

    if (!nm_logging_setup(global_opt.opt_log_level,
                          global_opt.opt_log_domains,
                          &bad_domains,
                          &error)) {
        fprintf(stderr,
                _("%s.  Please use --help to see a list of valid options.\n"),
                error->message);
        return 1;
    } else if (bad_domains) {
        fprintf(stderr,
                _("Ignoring unrecognized log domain(s) '%s' passed on command line.\n"),
                bad_domains);
        nm_clear_g_free(&bad_domains);
    }

    if (global_opt.become_daemon && !global_opt.debug) {
        if (daemon(0, 0) < 0) {
            errsv = errno;
            fprintf(stderr,
                    _("Could not daemonize: %s [error %u]\n"),
                    nm_strerror_native(errsv),
                    errsv);
            return 1;
        }
        if (nm_main_utils_write_pidfile(pidfile))
            wrote_pidfile = TRUE;
    }

    /* Set up unix signal handling - before creating threads, but after daemonizing! */
    gl.main_loop = g_main_loop_new(NULL, FALSE);
    setup_signals();

    nm_logging_init(global_opt.logging_backend, global_opt.debug);

    _LOGI(LOGD_CORE, "nm-iface-helper (version " NM_DIST_VERSION ") is starting...");

    /* Set up platform interaction layer */
    nm_linux_platform_setup();

    pllink = nm_platform_link_get(NM_PLATFORM_GET, gl.ifindex);
    if (pllink) {
        hwaddr       = nmp_link_address_get_as_bytes(&pllink->l_address);
        bcast_hwaddr = nmp_link_address_get_as_bytes(&pllink->l_broadcast);

        if (pllink->master > 0) {
            gl.is_vrf_device =
                nm_platform_link_get_type(NM_PLATFORM_GET, pllink->master) == NM_LINK_TYPE_VRF;
        }
    }

    if (global_opt.iid_str) {
        GBytes *bytes;
        gsize   ignored = 0;

        bytes = nm_utils_hexstr2bin(global_opt.iid_str);
        if (!bytes || g_bytes_get_size(bytes) != sizeof(*iid)) {
            fprintf(stderr, _("(%s): Invalid IID %s\n"), global_opt.ifname, global_opt.iid_str);
            return 1;
        }
        iid = g_bytes_unref_to_data(bytes, &ignored);
    }

    if (global_opt.dhcp4_clientid) {
        /* this string is just a plain hex-string. Unlike ipv4.dhcp-client-id, which
         * is parsed via nm_dhcp_utils_client_id_string_to_bytes(). */
        client_id = nm_utils_hexstr2bin(global_opt.dhcp4_clientid);
        if (!client_id || g_bytes_get_size(client_id) < 2) {
            fprintf(stderr,
                    _("(%s): Invalid DHCP client-id %s\n"),
                    global_opt.ifname,
                    global_opt.dhcp4_clientid);
            return 1;
        }
    }

    if (global_opt.dhcp4_address) {
        nm_platform_sysctl_ip_conf_set(NM_PLATFORM_GET,
                                       AF_INET,
                                       global_opt.ifname,
                                       "promote_secondaries",
                                       "1");

        dhcp4_client = nm_dhcp_manager_start_ip4(nm_dhcp_manager_get(),
                                                 nm_platform_get_multi_idx(NM_PLATFORM_GET),
                                                 global_opt.ifname,
                                                 gl.ifindex,
                                                 hwaddr,
                                                 bcast_hwaddr,
                                                 global_opt.uuid,
                                                 RT_TABLE_MAIN,
                                                 global_opt.priority_v4,
                                                 !!global_opt.dhcp4_hostname,
                                                 global_opt.dhcp4_hostname,
                                                 global_opt.dhcp4_fqdn,
                                                 NM_DHCP_HOSTNAME_FLAGS_FQDN_DEFAULT_IP4,
                                                 global_opt.mud_url,
                                                 client_id,
                                                 NM_DHCP_TIMEOUT_DEFAULT,
                                                 NULL,
                                                 global_opt.dhcp4_address,
                                                 NULL,
                                                 NULL,
                                                 &error);
        if (!dhcp4_client)
            g_error("failure to start DHCP: %s", error->message);

        g_signal_connect(dhcp4_client,
                         NM_DHCP_CLIENT_SIGNAL_STATE_CHANGED,
                         G_CALLBACK(dhcp4_state_changed),
                         NULL);
    }

    if (global_opt.slaac) {
        NMUtilsStableType stable_type = NM_UTILS_STABLE_TYPE_UUID;
        const char *      stable_id   = global_opt.uuid;
        int               router_solicitation_interval;
        int               router_solicitations;
        guint32           default_ra_timeout;
        int               max_addresses;

        nm_platform_link_set_user_ipv6ll_enabled(NM_PLATFORM_GET, gl.ifindex, TRUE);

        if (global_opt.stable_id
            && (global_opt.stable_id[0] >= '0' && global_opt.stable_id[0] <= '9')
            && global_opt.stable_id[1] == ' ') {
            /* strict parsing of --stable-id, which is the numeric stable-type
             * and the ID, joined with one space. For now, only support stable-types
             * from 0 to 9. */
            stable_type = (global_opt.stable_id[0] - '0');
            stable_id   = &global_opt.stable_id[2];
        }

        nm_lndp_ndisc_get_sysctl(NM_PLATFORM_GET,
                                 global_opt.ifname,
                                 &max_addresses,
                                 &router_solicitations,
                                 &router_solicitation_interval,
                                 &default_ra_timeout);

        ndisc = nm_lndp_ndisc_new(NM_PLATFORM_GET,
                                  gl.ifindex,
                                  global_opt.ifname,
                                  stable_type,
                                  stable_id,
                                  global_opt.addr_gen_mode,
                                  NM_NDISC_NODE_TYPE_HOST,
                                  max_addresses,
                                  router_solicitations,
                                  router_solicitation_interval,
                                  default_ra_timeout,
                                  NULL);
        g_assert(ndisc);

        if (iid)
            nm_ndisc_set_iid(ndisc, *iid);

        nm_platform_sysctl_ip_conf_set(NM_PLATFORM_GET,
                                       AF_INET6,
                                       global_opt.ifname,
                                       "accept_ra",
                                       "0");

        g_signal_connect(NM_PLATFORM_GET,
                         NM_PLATFORM_SIGNAL_IP6_ADDRESS_CHANGED,
                         G_CALLBACK(ip6_address_changed),
                         ndisc);
        g_signal_connect(ndisc, NM_NDISC_CONFIG_RECEIVED, G_CALLBACK(ndisc_config_changed), NULL);
        g_signal_connect(ndisc, NM_NDISC_RA_TIMEOUT_SIGNAL, G_CALLBACK(ndisc_ra_timeout), NULL);
        nm_ndisc_start(ndisc);
    }

    sd_id = nm_sd_event_attach_default();

    g_main_loop_run(gl.main_loop);

    nm_clear_g_source(&gl.dad_failed_id);
    nm_c_list_elem_free_all(&gl.dad_failed_lst_head, (GDestroyNotify) nmp_object_unref);

    if (pidfile && wrote_pidfile)
        unlink(pidfile);

    _LOGI(LOGD_CORE, "exiting");

    nm_clear_g_source(&sd_id);
    nm_clear_pointer(&gl.main_loop, g_main_loop_unref);
    return 0;
}

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

const NMDhcpClientFactory *const _nm_dhcp_manager_factories[6] = {
    /* For nm-iface-helper there is no option to choose a DHCP plugin.
     * It just uses the "internal" one. */
    &_nm_dhcp_client_factory_internal,
};

/*****************************************************************************/
/* Stub functions */

#include "nm-config.h"
#include "devices/nm-device.h"
#include "nm-active-connection.h"
#include "nm-dbus-manager.h"

void
nm_main_config_reload(int signal)
{
    _LOGI(LOGD_CORE, "reloading configuration not supported");
}

NMConfig *
nm_config_get(void)
{
    return GUINT_TO_POINTER(1);
}

NMConfigData *
nm_config_get_data_orig(NMConfig *config)
{
    return GUINT_TO_POINTER(1);
}

char *
nm_config_data_get_value(const NMConfigData *  config_data,
                         const char *          group,
                         const char *          key,
                         NMConfigGetValueFlags flags)
{
    return NULL;
}

NMConfigConfigureAndQuitType
nm_config_get_configure_and_quit(NMConfig *config)
{
    return NM_CONFIG_CONFIGURE_AND_QUIT_ENABLED;
}

NMDBusManager *
nm_dbus_manager_get(void)
{
    return NULL;
}

gboolean
nm_dbus_manager_is_stopping(NMDBusManager *self)
{
    return FALSE;
}

void
_nm_dbus_manager_obj_export(NMDBusObject *obj)
{}

void
_nm_dbus_manager_obj_unexport(NMDBusObject *obj)
{}

void
_nm_dbus_manager_obj_notify(NMDBusObject *obj, guint n_pspecs, const GParamSpec *const *pspecs)
{}

void
_nm_dbus_manager_obj_emit_signal(NMDBusObject *                     obj,
                                 const NMDBusInterfaceInfoExtended *interface_info,
                                 const GDBusSignalInfo *            signal_info,
                                 GVariant *                         args)
{}

GType
nm_device_get_type(void)
{
    g_return_val_if_reached(0);
}

GType
nm_active_connection_get_type(void)
{
    g_return_val_if_reached(0);
}