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

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

#include "nm-policy.h"

#include <unistd.h>
#include <netdb.h>

#include "libnm-core-intern/nm-core-internal.h"
#include "libnm-platform/nm-platform.h"
#include "libnm-platform/nmp-object.h"

#include "NetworkManagerUtils.h"
#include "devices/nm-device.h"
#include "devices/nm-device-factory.h"
#include "dns/nm-dns-manager.h"
#include "nm-act-request.h"
#include "nm-auth-utils.h"
#include "nm-config.h"
#include "nm-dhcp-config.h"
#include "nm-dispatcher.h"
#include "nm-firewalld-manager.h"
#include "nm-hostname-manager.h"
#include "nm-keep-alive.h"
#include "nm-l3-config-data.h"
#include "nm-manager.h"
#include "nm-netns.h"
#include "nm-setting-connection.h"
#include "nm-setting-ip4-config.h"
#include "nm-utils.h"
#include "settings/nm-agent-manager.h"
#include "settings/nm-settings-connection.h"
#include "settings/nm-settings.h"
#include "vpn/nm-vpn-manager.h"

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

NM_GOBJECT_PROPERTIES_DEFINE(NMPolicy,
                             PROP_MANAGER,
                             PROP_SETTINGS,
                             PROP_DEFAULT_IP4_AC,
                             PROP_DEFAULT_IP6_AC,
                             PROP_ACTIVATING_IP4_AC,
                             PROP_ACTIVATING_IP6_AC, );

#define HOSTNAME_RETRY_INTERVAL_MIN        30U
#define HOSTNAME_RETRY_INTERVAL_MAX        (60U * 60 * 12) /* 12 hours */
#define HOSTNAME_RETRY_INTERVAL_MULTIPLIER 8U

typedef struct {
    NMManager          *manager;
    NMNetns            *netns;
    NMFirewalldManager *firewalld_manager;
    CList               policy_auto_activate_lst_head;

    NMAgentManager *agent_mgr;

    GHashTable *devices;
    GHashTable *pending_active_connections;

    GSList *pending_secondaries;

    NMSettings *settings;

    GSource *device_recheck_auto_activate_all_idle_source;

    GSource *reset_connections_retries_idle_source;

    NMHostnameManager *hostname_manager;

    NMActiveConnection *default_ac4, *activating_ac4;
    NMActiveConnection *default_ac6, *activating_ac6;

    NMDnsManager *dns_manager;
    gulong        config_changed_id;

    NMPolicyHostnameMode hostname_mode;
    char                *orig_hostname;     /* hostname at NM start time */
    char                *cur_hostname;      /* hostname we want to assign */
    char                *cur_hostname_full; /* similar to @last_hostname, but before shortening */
    char                *last_hostname;     /* last hostname NM set (to detect if someone else
                                             * changed it in the meanwhile) */
    struct {
        GSource *source;
        guint    interval_sec;
        gboolean do_restart; /* when something changes, set this to TRUE so that the next retry
                              * will restart from the lowest timeout. */
    } hostname_retry;

    bool changing_hostname : 1; /* hostname set operation in progress */
    bool dhcp_hostname : 1;     /* current hostname was set from dhcp */
    bool updating_dns : 1;

    GArray *ip6_prefix_delegations; /* pool of ip6 prefixes delegated to all devices */

} NMPolicyPrivate;

struct _NMPolicy {
    GObject         parent;
    NMPolicyPrivate _priv;
};

struct _NMPolicyClass {
    GObjectClass parent;
};

G_DEFINE_TYPE(NMPolicy, nm_policy, G_TYPE_OBJECT)

#define NM_POLICY_GET_PRIVATE(self) _NM_GET_PRIVATE(self, NMPolicy, NM_IS_POLICY)

static NMPolicy *
_PRIV_TO_SELF(NMPolicyPrivate *priv)
{
    NMPolicy *self;

    nm_assert(priv);

    self = NM_CAST_ALIGN(NMPolicy, (((char *) priv) - G_STRUCT_OFFSET(NMPolicy, _priv)));

    nm_assert(NM_IS_POLICY(self));

    return self;
}

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

#define _NMLOG_PREFIX_NAME "policy"
#undef _NMLOG_ENABLED
#define _NMLOG_ENABLED(level, domain) (nm_logging_enabled((level), (domain)))
#define _NMLOG(level, domain, ...)                                         \
    G_STMT_START                                                           \
    {                                                                      \
        nm_log((level),                                                    \
               (domain),                                                   \
               NULL,                                                       \
               NULL,                                                       \
               "%s" _NM_UTILS_MACRO_FIRST(__VA_ARGS__),                    \
               _NMLOG_PREFIX_NAME ": " _NM_UTILS_MACRO_REST(__VA_ARGS__)); \
    }                                                                      \
    G_STMT_END

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

static void update_system_hostname(NMPolicy *self, const char *msg, gboolean reset_retry_interval);
static void nm_policy_device_recheck_auto_activate_all_schedule(NMPolicy *self);
static NMDevice *get_default_device(NMPolicy *self, int addr_family);
static gboolean  hostname_retry_cb(gpointer user_data);

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

typedef struct {
    NMPlatformIP6Address prefix;
    NMDevice            *device;      /* The requesting ("uplink") device */
    guint64              next_subnet; /* Cache of the next subnet number to be
                                       * assigned from this prefix */
    GHashTable          *subnets;     /* ifindex -> NMPlatformIP6Address */
} IP6PrefixDelegation;

static void
_clear_ip6_subnet(gpointer key, gpointer value, gpointer user_data)
{
    NMPlatformIP6Address *subnet = value;
    NMDevice *device = nm_manager_get_device_by_ifindex(NM_MANAGER_GET, GPOINTER_TO_INT(key));

    if (device) {
        /* We can not remove a subnet we already started announcing.
         * Just un-prefer it. */
        subnet->preferred = 0;
        nm_device_use_ip6_subnet(device, subnet);
    }
    g_slice_free(NMPlatformIP6Address, subnet);
}

static void
clear_ip6_prefix_delegation(gpointer data)
{
    IP6PrefixDelegation *delegation = data;
    char                 sbuf[NM_INET_ADDRSTRLEN];

    _LOGD(LOGD_IP6,
          "ipv6-pd: undelegating prefix %s/%d",
          nm_inet6_ntop(&delegation->prefix.address, sbuf),
          delegation->prefix.plen);

    g_hash_table_foreach(delegation->subnets, _clear_ip6_subnet, NULL);
    g_hash_table_destroy(delegation->subnets);
}

static void
expire_ip6_delegations(NMPolicy *self)
{
    NMPolicyPrivate     *priv       = NM_POLICY_GET_PRIVATE(self);
    guint32              now        = nm_utils_get_monotonic_timestamp_sec();
    IP6PrefixDelegation *delegation = NULL;
    guint                i;

    for (i = 0; i < priv->ip6_prefix_delegations->len; i++) {
        delegation = &nm_g_array_index(priv->ip6_prefix_delegations, IP6PrefixDelegation, i);
        if (delegation->prefix.timestamp + delegation->prefix.lifetime < now)
            g_array_remove_index_fast(priv->ip6_prefix_delegations, i);
    }
}

/*
 * Try to obtain a new subnet for a particular active connection from given
 * delegated prefix, possibly reusing the existing subnet.
 * Return value of FALSE indicates no more subnets are available from
 * this prefix (and other prefix should be used -- and requested if necessary).
 */
static gboolean
ip6_subnet_from_delegation(IP6PrefixDelegation *delegation, NMDevice *device)
{
    NMPlatformIP6Address *subnet;
    int                   ifindex = nm_device_get_ifindex(device);
    char                  sbuf[NM_INET_ADDRSTRLEN];

    subnet = g_hash_table_lookup(delegation->subnets, GINT_TO_POINTER(ifindex));
    if (!subnet) {
        /* Check for out-of-prefixes condition. */
        if (delegation->next_subnet >= (1 << (64 - delegation->prefix.plen))) {
            _LOGD(LOGD_IP6,
                  "ipv6-pd: no more prefixes in %s/%d",
                  nm_inet6_ntop(&delegation->prefix.address, sbuf),
                  delegation->prefix.plen);
            return FALSE;
        }

        /* Allocate a new subnet. */
        subnet = g_slice_new0(NMPlatformIP6Address);
        g_hash_table_insert(delegation->subnets, GINT_TO_POINTER(ifindex), subnet);

        subnet->plen = 64;
        subnet->address.s6_addr32[0] =
            delegation->prefix.address.s6_addr32[0] | htonl(delegation->next_subnet >> 32);
        subnet->address.s6_addr32[1] =
            delegation->prefix.address.s6_addr32[1] | htonl(delegation->next_subnet);

        /* Out subnet pool management is pretty unsophisticated. We only add
         * the subnets and index them by ifindex. That keeps the implementation
         * simple and the dead entries make it easy to reuse the same subnet on
         * subsequent activations. On the other hand they may waste the subnet
         * space. */
        delegation->next_subnet++;
    }

    subnet->timestamp = delegation->prefix.timestamp;
    subnet->lifetime  = delegation->prefix.lifetime;
    subnet->preferred = delegation->prefix.preferred;

    _LOGD(LOGD_IP6,
          "ipv6-pd: %s allocated from a /%d prefix on %s",
          nm_inet6_ntop(&subnet->address, sbuf),
          delegation->prefix.plen,
          nm_device_get_iface(device));

    nm_device_use_ip6_subnet(device, subnet);

    return TRUE;
}

/*
 * Try to obtain a subnet from each prefix delegated to given requesting
 * ("uplink") device and assign it to the downlink device.
 * Requests a new prefix if no subnet could be found.
 */
static void
ip6_subnet_from_device(NMPolicy *self, NMDevice *from_device, NMDevice *device)
{
    NMPolicyPrivate     *priv          = NM_POLICY_GET_PRIVATE(self);
    IP6PrefixDelegation *delegation    = NULL;
    gboolean             got_subnet    = FALSE;
    guint                have_prefixes = 0;
    guint                i;

    expire_ip6_delegations(self);

    for (i = 0; i < priv->ip6_prefix_delegations->len; i++) {
        delegation = &nm_g_array_index(priv->ip6_prefix_delegations, IP6PrefixDelegation, i);

        if (delegation->device != from_device)
            continue;

        if (ip6_subnet_from_delegation(delegation, device))
            got_subnet = TRUE;
        have_prefixes++;
    }

    if (!got_subnet) {
        _LOGI(LOGD_IP6,
              "ipv6-pd: none of %u prefixes of %s can be shared on %s",
              have_prefixes,
              nm_device_get_iface(from_device),
              nm_device_get_iface(device));
        nm_device_request_ip6_prefixes(from_device, have_prefixes + 1);
    }
}

static void
ip6_remove_device_prefix_delegations(NMPolicy *self, NMDevice *device)
{
    NMPolicyPrivate     *priv       = NM_POLICY_GET_PRIVATE(self);
    IP6PrefixDelegation *delegation = NULL;
    guint                i;

    for (i = 0; i < priv->ip6_prefix_delegations->len; i++) {
        delegation = &nm_g_array_index(priv->ip6_prefix_delegations, IP6PrefixDelegation, i);
        if (delegation->device == device)
            g_array_remove_index_fast(priv->ip6_prefix_delegations, i);
    }
}

static void
device_ip6_prefix_delegated(NMDevice                   *device,
                            const NMPlatformIP6Address *prefix,
                            gpointer                    user_data)
{
    NMPolicyPrivate     *priv       = user_data;
    NMPolicy            *self       = _PRIV_TO_SELF(priv);
    IP6PrefixDelegation *delegation = NULL;
    guint                i;
    const CList         *tmp_list;
    NMActiveConnection  *ac;
    char                 sbuf[NM_INET_ADDRSTRLEN];

    _LOGI(LOGD_IP6,
          "ipv6-pd: received a prefix %s/%d from %s",
          nm_inet6_ntop(&prefix->address, sbuf),
          prefix->plen,
          nm_device_get_iface(device));

    expire_ip6_delegations(self);

    for (i = 0; i < priv->ip6_prefix_delegations->len; i++) {
        /* Look for an already known prefix to update. */
        delegation = &nm_g_array_index(priv->ip6_prefix_delegations, IP6PrefixDelegation, i);
        if (IN6_ARE_ADDR_EQUAL(&delegation->prefix.address, &prefix->address))
            break;
    }

    if (i == priv->ip6_prefix_delegations->len) {
        /* Allocate a delegation for new prefix. */
        delegation = nm_g_array_append_new(priv->ip6_prefix_delegations, IP6PrefixDelegation);
        delegation->subnets     = g_hash_table_new(nm_direct_hash, NULL);
        delegation->next_subnet = 0;
    }

    delegation->device = device;
    delegation->prefix = *prefix;

    /* The newly activated connections are added to the end of the list,
     * so traversing it from the end makes it likely for newly
     * activated connections that have no subnet assigned to be served
     * first. That is a simple yet fair policy, which is good. */
    nm_manager_for_each_active_connection_prev (priv->manager, ac, tmp_list) {
        NMDevice *to_device;

        to_device = nm_active_connection_get_device(ac);
        if (nm_device_needs_ip6_subnet(to_device))
            ip6_subnet_from_delegation(delegation, to_device);
    }
}

static void
device_ip6_subnet_needed(NMDevice *device, gpointer user_data)
{
    NMPolicyPrivate *priv = user_data;
    NMPolicy        *self = _PRIV_TO_SELF(priv);

    _LOGD(LOGD_IP6, "ipv6-pd: %s needs a subnet", nm_device_get_iface(device));

    if (!priv->default_ac6) {
        /* We request the prefixes when the default IPv6 device is set. */
        _LOGI(LOGD_IP6,
              "ipv6-pd: no device to obtain a subnet to share on %s from",
              nm_device_get_iface(device));
        return;
    }
    ip6_subnet_from_device(self, get_default_device(self, AF_INET6), device);
    nm_device_copy_ip6_dns_config(device, get_default_device(self, AF_INET6));
}

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

static NMDevice *
get_default_device(NMPolicy *self, int addr_family)
{
    NMPolicyPrivate    *priv = NM_POLICY_GET_PRIVATE(self);
    NMActiveConnection *ac;

    nm_assert_addr_family(addr_family);

    ac = (addr_family == AF_INET) ? priv->default_ac4 : priv->default_ac6;

    return ac ? nm_active_connection_get_device(ac) : NULL;
}

static NMActiveConnection *
get_best_active_connection(NMPolicy *self, int addr_family, gboolean fully_activated)
{
    NMPolicyPrivate    *priv = NM_POLICY_GET_PRIVATE(self);
    const CList        *tmp_lst;
    NMDevice           *device;
    guint32             best_metric             = G_MAXUINT32;
    gboolean            best_is_fully_activated = FALSE;
    NMActiveConnection *best_ac, *prev_ac;

    nm_assert(NM_IN_SET(addr_family, AF_INET, AF_INET6));

    /* we prefer the current AC in case of identical metric.
     * Hence, try that one first. */
    prev_ac = addr_family == AF_INET ? (fully_activated ? priv->default_ac4 : priv->activating_ac4)
                                     : (fully_activated ? priv->default_ac6 : priv->activating_ac6);
    best_ac = NULL;

    nm_manager_for_each_device (priv->manager, device, tmp_lst) {
        NMDeviceState       state;
        const NMPObject    *r;
        NMActiveConnection *ac;
        NMConnection       *connection;
        guint32             metric;
        gboolean            is_fully_activated;

        state = nm_device_get_state(device);
        if (state <= NM_DEVICE_STATE_DISCONNECTED || state >= NM_DEVICE_STATE_DEACTIVATING)
            continue;

        if (nm_device_managed_type_is_external(device))
            continue;

        r = nm_device_get_best_default_route(device, addr_family);
        if (r) {
            /* NOTE: the best route might have rt_source NM_IP_CONFIG_SOURCE_VPN,
             * which means it was injected by a VPN, not added by device.
             *
             * In this case, is it really the best device? Why do we even need the best
             * device?? */
            metric             = NMP_OBJECT_CAST_IP_ROUTE(r)->metric;
            is_fully_activated = TRUE;
        } else if (!fully_activated && (connection = nm_device_get_applied_connection(device))
                   && nm_utils_connection_has_default_route(connection, addr_family, NULL)) {
            metric             = nm_device_get_route_metric(device, addr_family);
            is_fully_activated = FALSE;
        } else
            continue;

        ac = (NMActiveConnection *) nm_device_get_act_request(device);
        nm_assert(ac);

        if (!best_ac || (!best_is_fully_activated && is_fully_activated)
            || (metric < best_metric || (metric == best_metric && ac == prev_ac))) {
            best_ac                 = ac;
            best_metric             = metric;
            best_is_fully_activated = is_fully_activated;
        }
    }

    if (!fully_activated && best_ac && best_is_fully_activated) {
        /* There's a best activating AC only if the best device
         * among all activating and already-activated devices is a
         * still-activating one. */
        return NULL;
    }

    return best_ac;
}

static gboolean
any_devices_active(NMPolicy *self)
{
    NMPolicyPrivate *priv = NM_POLICY_GET_PRIVATE(self);
    const CList     *tmp_lst;
    NMDevice        *device;

    nm_manager_for_each_device (priv->manager, device, tmp_lst) {
        NMDeviceState state;

        state = nm_device_get_state(device);
        if (state <= NM_DEVICE_STATE_DISCONNECTED || state >= NM_DEVICE_STATE_DEACTIVATING)
            continue;
        if (nm_device_managed_type_is_external(device))
            continue;
        return TRUE;
    }
    return FALSE;
}

#define FALLBACK_HOSTNAME4 "localhost.localdomain"

static void
settings_set_hostname_cb(const char *hostname, gboolean result, gpointer user_data)
{
    NMPolicy        *self = NM_POLICY(user_data);
    NMPolicyPrivate *priv = NM_POLICY_GET_PRIVATE(self);
    int              ret  = 0;
    int              errsv;

    if (!result) {
        _LOGT(LOGD_DNS, "set-hostname: hostname set via dbus failed, fallback to \"sethostname\"");
        ret = sethostname(hostname, strlen(hostname));
        if (ret != 0) {
            errsv = errno;
            _LOGW(LOGD_DNS,
                  "set-hostname: couldn't set the system hostname to '%s': (%d) %s",
                  hostname,
                  errsv,
                  nm_strerror_native(errsv));
            if (errsv == EPERM)
                _LOGW(
                    LOGD_DNS,
                    "set-hostname: you should use hostnamed when systemd hardening is in effect!");
        }
    }

    priv->changing_hostname = FALSE;
    if (!ret)
        nm_dispatcher_call_hostname(NULL, NULL, NULL);
    g_object_unref(self);
}

#define HOST_NAME_BUFSIZE (HOST_NAME_MAX + 2)

static char *
_get_hostname(NMPolicy *self)
{
    NMPolicyPrivate *priv     = NM_POLICY_GET_PRIVATE(self);
    char            *hostname = NULL;
    int              errsv;

    /* If there is an in-progress hostname change, return
     * the last hostname set as would be set soon...
     */
    if (priv->changing_hostname) {
        _LOGT(LOGD_DNS, "get-hostname: \"%s\" (last on set)", priv->last_hostname);
        return g_strdup(priv->last_hostname);
    }

    /* try to get the hostname via dbus... */
    if (nm_hostname_manager_get_transient_hostname(priv->hostname_manager, &hostname)) {
        _LOGT(LOGD_DNS, "get-hostname: \"%s\" (from dbus)", hostname);
        return hostname;
    }

    /* ...or retrieve it by yourself */
    hostname = g_malloc(HOST_NAME_BUFSIZE);
    if (gethostname(hostname, HOST_NAME_BUFSIZE - 1) != 0) {
        errsv = errno;
        _LOGT(LOGD_DNS,
              "get-hostname: couldn't get the system hostname: (%d) %s",
              errsv,
              nm_strerror_native(errsv));
        g_free(hostname);
        return NULL;
    }

    /* the name may be truncated... */
    hostname[HOST_NAME_BUFSIZE - 1] = '\0';
    if (strlen(hostname) >= HOST_NAME_BUFSIZE - 1) {
        _LOGT(LOGD_DNS, "get-hostname: system hostname too long: \"%s\"", hostname);
        g_free(hostname);
        return NULL;
    }

    _LOGT(LOGD_DNS, "get-hostname: \"%s\"", hostname);
    return hostname;
}

static void
hostname_retry_schedule(NMPolicy *self)
{
    NMPolicyPrivate *priv = NM_POLICY_GET_PRIVATE(self);

    if (priv->hostname_retry.source && !priv->hostname_retry.do_restart)
        return;

    nm_clear_g_source_inst(&priv->hostname_retry.source);

    if (priv->hostname_retry.do_restart)
        priv->hostname_retry.interval_sec = 0;

    priv->hostname_retry.interval_sec *= HOSTNAME_RETRY_INTERVAL_MULTIPLIER;
    priv->hostname_retry.interval_sec = NM_CLAMP(priv->hostname_retry.interval_sec,
                                                 HOSTNAME_RETRY_INTERVAL_MIN,
                                                 HOSTNAME_RETRY_INTERVAL_MAX);

    _LOGT(LOGD_DNS,
          "hostname-retry: schedule in %u seconds%s",
          priv->hostname_retry.interval_sec,
          priv->hostname_retry.do_restart ? " (restarted)" : "");
    priv->hostname_retry.source =
        nm_g_timeout_add_seconds_source(priv->hostname_retry.interval_sec, hostname_retry_cb, self);

    priv->hostname_retry.do_restart = FALSE;
}

static gboolean
hostname_retry_cb(gpointer user_data)
{
    NMPolicy        *self = NM_POLICY(user_data);
    NMPolicyPrivate *priv = NM_POLICY_GET_PRIVATE(self);
    const CList     *tmp_lst;
    NMDevice        *device;

    _LOGT(LOGD_DNS, "hostname-retry: timeout");

    nm_clear_g_source_inst(&priv->hostname_retry.source);

    /* Clear any cached DNS results before retrying */
    nm_manager_for_each_device (priv->manager, device, tmp_lst) {
        nm_device_clear_dns_lookup_data(device, "hostname retry timeout");
    }
    update_system_hostname(self, "hostname retry timeout", FALSE);

    return G_SOURCE_CONTINUE;
}

static void
_set_hostname(NMPolicy *self, const char *new_hostname, const char *msg, gboolean do_retry)
{
    NMPolicyPrivate *priv         = NM_POLICY_GET_PRIVATE(self);
    gs_free char    *old_hostname = NULL;
    gboolean         cur_hostname_full_changed;
    const char      *name;

    /* The incoming hostname *can* be NULL, which will get translated to
     * 'localhost.localdomain' or such in the hostname policy code, but we
     * keep cur_hostname = NULL in the case because we need to know that
     * there was no valid hostname to start with.
     */

    if (nm_strdup_reset(&priv->cur_hostname_full, new_hostname)) {
        gs_free char *shortened = NULL;

        cur_hostname_full_changed = TRUE;

        if (priv->cur_hostname_full
            && !nm_utils_shorten_hostname(priv->cur_hostname_full, &shortened)) {
            _LOGW(LOGD_DNS,
                  "set-hostname: hostname '%s' %s is invalid",
                  priv->cur_hostname_full,
                  msg);
            return;
        }

        if (shortened) {
            _LOGI(LOGD_DNS,
                  "set-hostname: shortened hostname %s from '%s' to '%s'",
                  msg,
                  priv->cur_hostname_full,
                  shortened);
            nm_strdup_reset_take(&priv->cur_hostname, g_steal_pointer(&shortened));
        } else
            nm_strdup_reset(&priv->cur_hostname, priv->cur_hostname_full);
    } else
        cur_hostname_full_changed = FALSE;

    /* Update the DNS only if the hostname is actually
     * going to change.
     */
    if (cur_hostname_full_changed) {
        /* Notify the DNS manager of the hostname change so that the domain part, if
         * present, can be added to the search list. Set the @updating_dns flag
         * so that dns_config_changed() doesn't try again to restart DNS lookup.
         */
        priv->updating_dns = TRUE;
        nm_dns_manager_set_hostname(priv->dns_manager,
                                    priv->cur_hostname_full,
                                    !any_devices_active(self));
        priv->updating_dns = FALSE;
    }

    if (!do_retry) {
        _LOGT(LOGD_DNS, "hostname-retry: clear");
        nm_clear_g_source_inst(&priv->hostname_retry.source);
        priv->hostname_retry.interval_sec = 0;
        priv->hostname_retry.do_restart   = FALSE;
    } else if (!priv->hostname_retry.source) {
        hostname_retry_schedule(self);
    }

    /* Finally, set kernel hostname */
    nm_assert(!priv->cur_hostname || priv->cur_hostname[0]);
    name = priv->cur_hostname ?: FALLBACK_HOSTNAME4;

    /* Don't set the hostname if it isn't actually changing */
    if ((old_hostname = _get_hostname(self)) && (nm_streq(name, old_hostname))) {
        _LOGT(LOGD_DNS, "set-hostname: hostname already set to '%s' (%s)", name, msg);
        return;
    }

    /* Keep track of the last set hostname */
    nm_strdup_reset(&priv->last_hostname, name);
    priv->changing_hostname = TRUE;

    _LOGI(LOGD_DNS, "set-hostname: set hostname to '%s' (%s)", name, msg);

    /* Ask NMSettings to update the transient hostname using its
     * systemd-hostnamed proxy.
     *
     * FIXME(shutdown): an async request must be cancellable, so we are guaranteed
     *   to tear down in a reasonable time.*/
    nm_hostname_manager_set_transient_hostname(priv->hostname_manager,
                                               name,
                                               settings_set_hostname_cb,
                                               g_object_ref(self));
}

typedef struct {
    NMDevice *device;
    int       priority;
    bool      from_dhcp : 1;
    bool      from_dns : 1;
    bool      IS_IPv4 : 1;
    bool      is_default : 1;
} DeviceHostnameInfo;

static int
device_hostname_info_compare(gconstpointer a, gconstpointer b)
{
    const DeviceHostnameInfo *info1 = a;
    const DeviceHostnameInfo *info2 = b;

    NM_CMP_FIELD(info1, info2, priority);
    NM_CMP_FIELD_UNSAFE(info2, info1, is_default);
    NM_CMP_FIELD_UNSAFE(info2, info1, IS_IPv4);

    return 0;
}

NM_CON_DEFAULT_NOP("hostname.from-dhcp");
NM_CON_DEFAULT_NOP("hostname.from-dns-lookup");
NM_CON_DEFAULT_NOP("hostname.only-from-default");

static gboolean
device_get_hostname_property_boolean(NMDevice *device, const char *name)
{
    NMSettingHostname *s_hostname;
    char               buf[128];
    int                value;
    NMTernary          default_value;

    nm_assert(NM_IN_STRSET(name,
                           NM_SETTING_HOSTNAME_FROM_DHCP,
                           NM_SETTING_HOSTNAME_FROM_DNS_LOOKUP,
                           NM_SETTING_HOSTNAME_ONLY_FROM_DEFAULT));

    s_hostname = nm_device_get_applied_setting(device, NM_TYPE_SETTING_HOSTNAME);

    if (s_hostname) {
        g_object_get(s_hostname, name, &value, NULL);
        if (NM_IN_SET(value, NM_TERNARY_FALSE, NM_TERNARY_TRUE))
            return value;
    }

    if (nm_streq(name, NM_SETTING_HOSTNAME_ONLY_FROM_DEFAULT))
        default_value = NM_TERNARY_FALSE;
    else
        default_value = NM_TERNARY_TRUE;

    return nm_config_data_get_connection_default_int64(NM_CONFIG_GET_DATA,
                                                       nm_sprintf_buf(buf, "hostname.%s", name),
                                                       device,
                                                       NM_TERNARY_FALSE,
                                                       NM_TERNARY_TRUE,
                                                       default_value);
}

static int
device_get_hostname_priority(NMDevice *device)
{
    NMSettingHostname *s_hostname;
    int                priority;

    s_hostname = nm_device_get_applied_setting(device, NM_TYPE_SETTING_HOSTNAME);
    if (s_hostname) {
        priority = nm_setting_hostname_get_priority(s_hostname);
        if (priority != 0)
            return priority;
    }

    return nm_config_data_get_connection_default_int64(NM_CONFIG_GET_DATA,
                                                       NM_CON_DEFAULT("hostname.priority"),
                                                       device,
                                                       G_MININT,
                                                       G_MAXINT,
                                                       100);
}

static GArray *
build_device_hostname_infos(NMPolicy *self)
{
    NMPolicyPrivate    *priv = NM_POLICY_GET_PRIVATE(self);
    const CList        *tmp_clist;
    NMActiveConnection *ac;
    GArray             *array = NULL;

    nm_manager_for_each_active_connection (priv->manager, ac, tmp_clist) {
        DeviceHostnameInfo *info;
        NMDevice           *device;
        gboolean            only_from_default;
        gboolean            is_default;
        int                 IS_IPv4;

        device = nm_active_connection_get_device(ac);
        if (!device)
            continue;

        if (nm_device_managed_type_is_external(device))
            continue;

        only_from_default =
            device_get_hostname_property_boolean(device, NM_SETTING_HOSTNAME_ONLY_FROM_DEFAULT);

        for (IS_IPv4 = 1; IS_IPv4 >= 0; IS_IPv4--) {
            is_default = (ac == (IS_IPv4 ? priv->default_ac4 : priv->default_ac6));
            if (only_from_default && !is_default)
                continue;

            if (!array)
                array = g_array_sized_new(FALSE, FALSE, sizeof(DeviceHostnameInfo), 4);

            info  = nm_g_array_append_new(array, DeviceHostnameInfo);
            *info = (DeviceHostnameInfo) {
                .device   = device,
                .priority = device_get_hostname_priority(device),
                .from_dhcp =
                    device_get_hostname_property_boolean(device, NM_SETTING_HOSTNAME_FROM_DHCP),
                .from_dns =
                    device_get_hostname_property_boolean(device,
                                                         NM_SETTING_HOSTNAME_FROM_DNS_LOOKUP),
                .IS_IPv4    = IS_IPv4,
                .is_default = is_default,
            };
        }
    }

    if (array && array->len > 1) {
        const DeviceHostnameInfo *info0;
        guint                     i;

        g_array_sort(array, device_hostname_info_compare);

        info0 = &nm_g_array_first(array, DeviceHostnameInfo);
        if (info0->priority < 0) {
            for (i = 1; i < array->len; i++) {
                const DeviceHostnameInfo *info = &nm_g_array_index(array, DeviceHostnameInfo, i);

                if (info->priority > info0->priority) {
                    g_array_set_size(array, i);
                    break;
                }
            }
        }
    }

    return array;
}

static void
device_dns_lookup_done(NMDevice *device, gpointer user_data)
{
    NMPolicy *self = user_data;

    g_signal_handlers_disconnect_by_func(device, device_dns_lookup_done, self);

    update_system_hostname(self, "lookup finished", FALSE);
}

static void
device_carrier_changed(NMDevice *device, GParamSpec *pspec, gpointer user_data)
{
    NMPolicyPrivate *priv = user_data;
    NMPolicy        *self = _PRIV_TO_SELF(priv);
    gs_free char    *msg  = NULL;

    if (nm_device_has_carrier(device)) {
        g_signal_handlers_disconnect_by_func(device, device_carrier_changed, priv);
        msg = g_strdup_printf("device '%s' got carrier", nm_device_get_iface(device));
        update_system_hostname(self, msg, TRUE);
    }
}

/*
 * This function evaluates different sources (static configuration, DHCP, DNS, ...)
 * to set the system hostname.
 *
 * When the function needs to perform a blocking action like a DNS resolution, it
 * subscribes to a signal for the completion event, registering a callback that
 * invokes this function again. In the new invocation, any previous DNS result is
 * cached and doesn't need a new resolution.
 *
 * In case no hostname is found when after sources have been evaluated, it schedules
 * a timer to retry later with an interval that is increased at each attempt. When
 * this function is called after something changed (for example, carrier went up, a
 * new address was added), @reset_retry_interval should be set to TRUE so that the
 * next retry will use the smallest interval. In this way, it can quickly adapt to
 * temporary misconfigurations at boot or when the network environment changes.
 */
static void
update_system_hostname(NMPolicy *self, const char *msg, gboolean reset_retry_interval)
{
    NMPolicyPrivate       *priv = NM_POLICY_GET_PRIVATE(self);
    const char            *configured_hostname;
    gs_free char          *temp_hostname = NULL;
    const char            *dhcp_hostname, *p;
    gboolean               external_hostname = FALSE;
    NMDhcpConfig          *dhcp_config;
    gs_unref_array GArray *infos = NULL;
    DeviceHostnameInfo    *info;
    guint                  i;
    int                    addr_family;

    g_return_if_fail(self != NULL);

    if (reset_retry_interval)
        priv->hostname_retry.do_restart = TRUE;

    if (priv->hostname_mode == NM_POLICY_HOSTNAME_MODE_NONE) {
        _LOGT(LOGD_DNS, "set-hostname: hostname is unmanaged");
        return;
    }

    _LOGT(LOGD_DNS, "set-hostname: updating hostname (%s)", msg);

    /* Check if the hostname was set externally to NM, so that in that case
     * we can avoid to fallback to the one we got when we started.
     * Consider "not specific" hostnames as equal. */
    if ((temp_hostname = _get_hostname(self)) && !nm_streq0(temp_hostname, priv->last_hostname)
        && (nm_utils_is_specific_hostname(temp_hostname)
            || nm_utils_is_specific_hostname(priv->last_hostname))) {
        external_hostname = TRUE;
        _LOGD(LOGD_DNS,
              "set-hostname: current hostname was changed outside NetworkManager: '%s'",
              temp_hostname);
        priv->dhcp_hostname = FALSE;

        if (!nm_utils_is_specific_hostname(temp_hostname))
            nm_clear_g_free(&temp_hostname);
        if (!nm_streq0(temp_hostname, priv->orig_hostname)) {
            /* Update original (fallback) hostname */
            g_free(priv->orig_hostname);
            priv->orig_hostname = g_steal_pointer(&temp_hostname);
            _LOGT(LOGD_DNS,
                  "hostname-original: update to %s%s%s",
                  NM_PRINT_FMT_QUOTE_STRING(priv->orig_hostname));
        }
    }

    /* Hostname precedence order:
     *
     * 1) a configured hostname (from settings)
     * 2) automatic hostname from DHCP of eligible interfaces
     * 3) reverse-DNS lookup of the first address on eligible interfaces
     * 4) the last hostname set outside NM
     */

    /* Try a persistent hostname first */
    configured_hostname = nm_hostname_manager_get_static_hostname(priv->hostname_manager);
    if (configured_hostname && nm_utils_is_specific_hostname(configured_hostname)) {
        _set_hostname(self, configured_hostname, "from system configuration", FALSE);
        priv->dhcp_hostname = FALSE;
        return;
    }

    infos = build_device_hostname_infos(self);

    if (infos && _LOGT_ENABLED(LOGD_DNS)) {
        _LOGT(LOGD_DNS, "device hostname info:");
        for (i = 0; i < infos->len; i++) {
            info = &nm_g_array_index(infos, DeviceHostnameInfo, i);
            _LOGT(LOGD_DNS,
                  "  - prio:%5d ipv%c%s %s %s dev:%s",
                  info->priority,
                  info->IS_IPv4 ? '4' : '6',
                  info->is_default ? " (def)" : "      ",
                  info->from_dhcp ? "dhcp " : "     ",
                  info->from_dns ? "dns " : "    ",
                  nm_device_get_iface(info->device));
        }
    }

    for (i = 0; infos && i < infos->len; i++) {
        info        = &nm_g_array_index(infos, DeviceHostnameInfo, i);
        addr_family = info->IS_IPv4 ? AF_INET : AF_INET6;
        g_signal_handlers_disconnect_by_func(info->device, device_dns_lookup_done, self);
        g_signal_handlers_disconnect_by_func(info->device, device_carrier_changed, priv);

        if (info->from_dhcp) {
            dhcp_config = nm_device_get_dhcp_config(info->device, addr_family);
            if (dhcp_config) {
                dhcp_hostname =
                    nm_dhcp_config_get_option(dhcp_config,
                                              info->IS_IPv4 ? "host_name" : "fqdn_fqdn");
                if (dhcp_hostname && dhcp_hostname[0]) {
                    p = nm_str_skip_leading_spaces(dhcp_hostname);
                    if (p[0]) {
                        _set_hostname(self,
                                      p,
                                      info->IS_IPv4 ? "from DHCPv4" : "from DHCPv6",
                                      FALSE);
                        priv->dhcp_hostname = TRUE;
                        return;
                    }
                    _LOGW(LOGD_DNS,
                          "set-hostname: DHCPv%c-provided hostname '%s' looks invalid; "
                          "ignoring it",
                          nm_utils_addr_family_to_char(addr_family),
                          dhcp_hostname);
                }
            }
        }

        if (priv->hostname_mode != NM_POLICY_HOSTNAME_MODE_DHCP) {
            if (info->from_dns) {
                const char *result = NULL;
                gboolean    wait   = FALSE;

                if (nm_device_has_carrier(info->device)) {
                    result =
                        nm_device_get_hostname_from_dns_lookup(info->device, addr_family, &wait);
                } else {
                    g_signal_connect(info->device,
                                     "notify::" NM_DEVICE_CARRIER,
                                     G_CALLBACK(device_carrier_changed),
                                     priv);
                }
                if (result) {
                    _set_hostname(self, result, "from address lookup", FALSE);
                    return;
                }
                if (wait) {
                    g_signal_connect(info->device,
                                     NM_DEVICE_DNS_LOOKUP_DONE,
                                     G_CALLBACK(device_dns_lookup_done),
                                     self);
                    return;
                }
            }
        }
    }

    /* If an hostname was set outside NetworkManager keep it */
    if (external_hostname) {
        hostname_retry_schedule(self);
        return;
    }

    if (priv->hostname_mode == NM_POLICY_HOSTNAME_MODE_DHCP) {
        /* In dhcp hostname-mode, the hostname is updated only if it comes from
         * a DHCP host-name option: if last set was from a host-name option and
         * we are here than that connection is gone (with its host-name option),
         * so reset the hostname to the previous value
         */
        if (priv->dhcp_hostname) {
            _set_hostname(self, priv->orig_hostname, "reset dhcp hostname", TRUE);
            priv->dhcp_hostname = FALSE;
        }
        return;
    }

    priv->dhcp_hostname = FALSE;

    /* If no automatically-configured hostname, try using the last hostname
     * set externally to NM
     */
    if (priv->orig_hostname) {
        _set_hostname(self, priv->orig_hostname, "from system startup", TRUE);
        return;
    }

    _set_hostname(self, NULL, "no hostname found", TRUE);
}

static void
update_default_ac(NMPolicy *self, int addr_family, NMActiveConnection *best)
{
    NMPolicyPrivate    *priv = NM_POLICY_GET_PRIVATE(self);
    const CList        *tmp_list;
    NMActiveConnection *ac;

    /* Clear the 'default[6]' flag on all active connections that aren't the new
     * default active connection.  We'll set the new default after; this ensures
     * we don't ever have two marked 'default[6]' simultaneously.
     */
    nm_manager_for_each_active_connection (priv->manager, ac, tmp_list) {
        if (ac != best)
            nm_active_connection_set_default(ac, addr_family, FALSE);
    }

    /* Mark new default active connection */
    if (best)
        nm_active_connection_set_default(best, addr_family, TRUE);
}

static const NML3ConfigData *
get_best_ip_config(NMPolicy            *self,
                   int                  addr_family,
                   const char         **out_ip_iface,
                   NMActiveConnection **out_ac,
                   NMDevice           **out_device,
                   NMVpnConnection    **out_vpn)
{
    NMPolicyPrivate      *priv      = NM_POLICY_GET_PRIVATE(self);
    const NML3ConfigData *l3cd_best = NULL;
    const CList          *tmp_list;
    NMActiveConnection   *ac;
    guint64               best_metric = G_MAXUINT64;
    NMVpnConnection      *best_vpn    = NULL;

    nm_assert(NM_IN_SET(addr_family, AF_INET, AF_INET6));

    nm_manager_for_each_active_connection (priv->manager, ac, tmp_list) {
        const NML3ConfigData *l3cd;
        NMVpnConnection      *candidate;
        NMVpnConnectionState  vpn_state;
        const NMPObject      *obj;
        guint32               metric;

        if (!NM_IS_VPN_CONNECTION(ac))
            continue;

        candidate = NM_VPN_CONNECTION(ac);

        vpn_state = nm_vpn_connection_get_vpn_state(candidate);
        if (vpn_state != NM_VPN_CONNECTION_STATE_ACTIVATED)
            continue;

        l3cd = nm_vpn_connection_get_l3cd(candidate);
        if (!l3cd)
            continue;

        obj = nm_l3_config_data_get_best_default_route(l3cd, addr_family);
        if (!obj)
            continue;

        metric = NMP_OBJECT_CAST_IPX_ROUTE(obj)->rx.metric;
        if (metric <= best_metric) {
            best_metric = metric;
            l3cd_best   = l3cd;
            best_vpn    = candidate;
        }
    }

    if (best_metric != G_MAXUINT64) {
        NM_SET_OUT(out_device, NULL);
        NM_SET_OUT(out_vpn, best_vpn);
        NM_SET_OUT(out_ac, NM_ACTIVE_CONNECTION(best_vpn));
        NM_SET_OUT(out_ip_iface, nm_vpn_connection_get_ip_iface(best_vpn, TRUE));
        return l3cd_best;
    }

    ac = get_best_active_connection(self, addr_family, TRUE);
    if (ac) {
        NMDevice *device = nm_active_connection_get_device(ac);

        nm_assert(NM_IS_DEVICE(device));

        NM_SET_OUT(out_device, device);
        NM_SET_OUT(out_vpn, NULL);
        NM_SET_OUT(out_ac, ac);
        NM_SET_OUT(out_ip_iface, nm_device_get_ip_iface(device));
        return nm_device_get_l3cd(device, TRUE);
    }

    NM_SET_OUT(out_device, NULL);
    NM_SET_OUT(out_vpn, NULL);
    NM_SET_OUT(out_ac, NULL);
    NM_SET_OUT(out_ip_iface, NULL);
    return NULL;
}

static void
update_ip4_routing(NMPolicy *self, gboolean force_update)
{
    NMPolicyPrivate    *priv     = NM_POLICY_GET_PRIVATE(self);
    NMDevice           *best     = NULL;
    NMVpnConnection    *vpn      = NULL;
    NMActiveConnection *best_ac  = NULL;
    const char         *ip_iface = NULL;

    /* Note that we might have an IPv4 VPN tunneled over an IPv6-only device,
     * so we can get (vpn != NULL && best == NULL).
     */
    if (!get_best_ip_config(self, AF_INET, &ip_iface, &best_ac, &best, &vpn)) {
        if (nm_clear_g_object(&priv->default_ac4)) {
            _LOGt(LOGD_DNS, "set-default-ac-4: %p", NULL);
            _notify(self, PROP_DEFAULT_IP4_AC);
        }
        return;
    }
    g_assert((best || vpn) && best_ac);

    if (!force_update && best_ac && best_ac == priv->default_ac4)
        return;

    if (best) {
        const CList        *tmp_list;
        NMActiveConnection *ac;

        nm_manager_for_each_active_connection (priv->manager, ac, tmp_list) {
            if (NM_IS_VPN_CONNECTION(ac) && !nm_active_connection_get_device(ac))
                nm_active_connection_set_device(ac, best);
        }
    }

    update_default_ac(self, AF_INET, best_ac);

    if (!nm_g_object_ref_set(&priv->default_ac4, best_ac))
        return;
    _LOGt(LOGD_DNS, "set-default-ac-4: %p", priv->default_ac4);

    _LOGI(LOGD_CORE,
          "set '%s' (%s) as default for IPv4 routing and DNS",
          nm_connection_get_id(nm_active_connection_get_applied_connection(best_ac)),
          ip_iface);
    _notify(self, PROP_DEFAULT_IP4_AC);
}

static void
update_ip6_prefix_delegation(NMPolicy *self)
{
    NMPolicyPrivate    *priv = NM_POLICY_GET_PRIVATE(self);
    NMDevice           *device;
    NMActiveConnection *ac;
    const CList        *tmp_list;

    /* There's new default IPv6 connection, try to get a prefix for everyone. */
    nm_manager_for_each_active_connection (priv->manager, ac, tmp_list) {
        device = nm_active_connection_get_device(ac);
        if (device && nm_device_needs_ip6_subnet(device))
            ip6_subnet_from_device(self, get_default_device(self, AF_INET6), device);
    }
}

static void
update_ip6_routing(NMPolicy *self, gboolean force_update)
{
    NMPolicyPrivate    *priv     = NM_POLICY_GET_PRIVATE(self);
    NMDevice           *best     = NULL;
    NMVpnConnection    *vpn      = NULL;
    NMActiveConnection *best_ac  = NULL;
    const char         *ip_iface = NULL;

    /* Note that we might have an IPv6 VPN tunneled over an IPv4-only device,
     * so we can get (vpn != NULL && best == NULL).
     */
    if (!get_best_ip_config(self, AF_INET6, &ip_iface, &best_ac, &best, &vpn)) {
        if (nm_clear_g_object(&priv->default_ac6)) {
            _LOGt(LOGD_DNS, "set-default-ac-6: %p", NULL);
            _notify(self, PROP_DEFAULT_IP6_AC);
        }
        return;
    }
    g_assert((best || vpn) && best_ac);

    if (!force_update && best_ac && best_ac == priv->default_ac6)
        return;

    if (best) {
        const CList        *tmp_list;
        NMActiveConnection *ac;

        nm_manager_for_each_active_connection (priv->manager, ac, tmp_list) {
            if (NM_IS_VPN_CONNECTION(ac) && !nm_active_connection_get_device(ac))
                nm_active_connection_set_device(ac, best);
        }
    }

    update_default_ac(self, AF_INET6, best_ac);

    if (!nm_g_object_ref_set(&priv->default_ac6, best_ac))
        return;
    _LOGt(LOGD_DNS, "set-default-ac-6: %p", priv->default_ac6);

    update_ip6_prefix_delegation(self);

    _LOGI(LOGD_CORE,
          "set '%s' (%s) as default for IPv6 routing and DNS",
          nm_connection_get_id(nm_active_connection_get_applied_connection(best_ac)),
          ip_iface);
    _notify(self, PROP_DEFAULT_IP6_AC);
}

static void
update_ip_dns(NMPolicy *self, int addr_family, NMDevice *changed_device)
{
    NMPolicyPrivate      *priv = NM_POLICY_GET_PRIVATE(self);
    const NML3ConfigData *l3cd;
    const char           *ip_iface = NULL;
    NMVpnConnection      *vpn      = NULL;
    NMDevice             *device   = NULL;

    nm_assert_addr_family(addr_family);

    l3cd = get_best_ip_config(self, addr_family, &ip_iface, NULL, &device, &vpn);
    if (l3cd) {
        NMDnsIPConfigType ip_config_type;

        nm_assert(!device || NM_IS_DEVICE(device));
        nm_assert(!vpn || NM_IS_VPN_CONNECTION(vpn));
        nm_assert((!!device) != (!!vpn));

        /* Tell the DNS manager this config is preferred by re-adding it with
         * a different IP config type.
         */
        if (device && nm_device_managed_type_is_external(device))
            ip_config_type = NM_DNS_IP_CONFIG_TYPE_REMOVED;
        else if (vpn || (device && nm_device_is_vpn(device)))
            ip_config_type = NM_DNS_IP_CONFIG_TYPE_VPN;
        else
            ip_config_type = NM_DNS_IP_CONFIG_TYPE_BEST_DEVICE;

        nm_dns_manager_set_ip_config(NM_POLICY_GET_PRIVATE(self)->dns_manager,
                                     addr_family,
                                     ((gconstpointer) device) ?: ((gconstpointer) vpn),
                                     l3cd,
                                     ip_config_type,
                                     TRUE);
    }

    if (addr_family == AF_INET6) {
        NMActiveConnection *ac;
        const CList        *tmp_list;

        /* Tell devices needing a subnet about the new DNS configuration */
        nm_manager_for_each_active_connection (priv->manager, ac, tmp_list) {
            device = nm_active_connection_get_device(ac);
            if (device && device != changed_device && nm_device_needs_ip6_subnet(device))
                nm_device_copy_ip6_dns_config(device, get_default_device(self, AF_INET6));
        }
    }
}

static void
update_routing_and_dns(NMPolicy *self, gboolean force_update, NMDevice *changed_device)
{
    NMPolicyPrivate *priv = NM_POLICY_GET_PRIVATE(self);

    nm_dns_manager_begin_updates(priv->dns_manager, __func__);

    update_ip_dns(self, AF_INET, changed_device);
    update_ip_dns(self, AF_INET6, changed_device);

    update_ip4_routing(self, force_update);
    update_ip6_routing(self, force_update);

    /* Update the system hostname */
    update_system_hostname(self, "routing and dns", FALSE);

    nm_dns_manager_end_updates(priv->dns_manager, __func__);
}

static void
check_activating_active_connections(NMPolicy *self)
{
    NMPolicyPrivate    *priv = NM_POLICY_GET_PRIVATE(self);
    NMActiveConnection *best4, *best6 = NULL;

    best4 = get_best_active_connection(self, AF_INET, FALSE);
    best6 = get_best_active_connection(self, AF_INET6, FALSE);

    g_object_freeze_notify(G_OBJECT(self));

    if (nm_g_object_ref_set(&priv->activating_ac4, best4)) {
        _LOGt(LOGD_DNS, "set-activating-ac-4: %p", priv->activating_ac4);
        _notify(self, PROP_ACTIVATING_IP4_AC);
    }
    if (nm_g_object_ref_set(&priv->activating_ac6, best6)) {
        _LOGt(LOGD_DNS, "set-activating-ac-6: %p", priv->activating_ac6);
        _notify(self, PROP_ACTIVATING_IP6_AC);
    }

    g_object_thaw_notify(G_OBJECT(self));
}

static void
pending_ac_gone(gpointer data, GObject *where_the_object_was)
{
    NMPolicy        *self = NM_POLICY(data);
    NMPolicyPrivate *priv = NM_POLICY_GET_PRIVATE(self);

    /* Active connections should reach the DEACTIVATED state
     * before disappearing. */
    nm_assert_not_reached();

    if (g_hash_table_remove(priv->pending_active_connections, where_the_object_was))
        g_object_unref(self);
}

static void
pending_ac_state_changed(NMActiveConnection *ac, guint state, guint reason, NMPolicy *self)
{
    NMPolicyPrivate      *priv = NM_POLICY_GET_PRIVATE(self);
    NMSettingsConnection *con;

    if (state >= NM_ACTIVE_CONNECTION_STATE_DEACTIVATING) {
        /* The AC is being deactivated before the device had a chance
         * to move to PREPARE. Schedule a new auto-activation on the
         * device, but block the current connection to avoid an activation
         * loop.
         */
        if (reason != NM_ACTIVE_CONNECTION_STATE_REASON_DEVICE_DISCONNECTED
            && reason != NM_ACTIVE_CONNECTION_STATE_REASON_CONNECTION_REMOVED) {
            con = nm_active_connection_get_settings_connection(ac);
            nm_manager_devcon_autoconnect_blocked_reason_set(
                priv->manager,
                nm_active_connection_get_device(ac),
                con,
                NM_SETTINGS_AUTOCONNECT_BLOCKED_REASON_FAILED,
                TRUE);
            nm_policy_device_recheck_auto_activate_schedule(self,
                                                            nm_active_connection_get_device(ac));
        }

        /* Cleanup */
        g_signal_handlers_disconnect_by_func(ac, pending_ac_state_changed, self);
        if (!g_hash_table_remove(priv->pending_active_connections, ac))
            nm_assert_not_reached();
        g_object_weak_unref(G_OBJECT(ac), pending_ac_gone, self);
        g_object_unref(self);
    }
}

static void
_auto_activate_device(NMPolicy *self, NMDevice *device)
{
    NMPolicyPrivate               *priv;
    NMSettingsConnection          *best_connection;
    gs_free char                  *specific_object = NULL;
    gs_free NMSettingsConnection **connections     = NULL;
    guint                          i, len;
    gs_free_error GError          *error   = NULL;
    gs_unref_object NMAuthSubject *subject = NULL;
    NMActiveConnection            *ac;

    nm_assert(NM_IS_POLICY(self));
    nm_assert(NM_IS_DEVICE(device));

    priv = NM_POLICY_GET_PRIVATE(self);

    // FIXME: if a device is already activating (or activated) with a connection
    // but another connection now overrides the current one for that device,
    // deactivate the device and activate the new connection instead of just
    // bailing if the device is already active
    if (nm_device_get_act_request(device)) {
        if (nm_device_managed_type_is_external(device)
            && nm_device_get_allow_autoconnect_on_external(device)) {
            /* this is an external activation, and we allow autoconnecting on
             * top of that.
             *
             * pass. */
        } else
            return;
    }

    if (!nm_device_autoconnect_allowed(device))
        return;

    connections = nm_manager_get_activatable_connections(priv->manager, TRUE, TRUE, &len);
    if (!connections[0])
        return;

    /* Find the first connection that should be auto-activated */
    best_connection = NULL;
    for (i = 0; i < len; i++) {
        NMSettingsConnection *candidate = connections[i];
        NMConnection         *cand_conn;
        NMSettingConnection  *s_con;
        const char           *permission;

        if (nm_manager_devcon_autoconnect_is_blocked(priv->manager, device, candidate))
            continue;

        cand_conn = nm_settings_connection_get_connection(candidate);

        s_con = nm_connection_get_setting_connection(cand_conn);
        if (!nm_setting_connection_get_autoconnect(s_con))
            continue;

        permission = nm_utils_get_shared_wifi_permission(cand_conn);
        if (permission && !nm_settings_connection_check_permission(candidate, permission))
            continue;

        if (nm_device_can_auto_connect(device, candidate, &specific_object)) {
            best_connection = candidate;
            break;
        }
    }

    if (!best_connection)
        return;

    _LOGI(LOGD_DEVICE,
          "auto-activating connection '%s' (%s)",
          nm_settings_connection_get_id(best_connection),
          nm_settings_connection_get_uuid(best_connection));

    subject = nm_auth_subject_new_internal();
    ac      = nm_manager_activate_connection(
        priv->manager,
        best_connection,
        NULL,
        specific_object,
        device,
        subject,
        NM_ACTIVATION_TYPE_MANAGED,
        NM_ACTIVATION_REASON_AUTOCONNECT,
        NM_ACTIVATION_STATE_FLAG_LIFETIME_BOUND_TO_PROFILE_VISIBILITY,
        &error);
    if (!ac) {
        _LOGI(LOGD_DEVICE,
              "connection '%s' auto-activation failed: %s",
              nm_settings_connection_get_id(best_connection),
              error->message);
        nm_manager_devcon_autoconnect_blocked_reason_set(
            priv->manager,
            device,
            best_connection,
            NM_SETTINGS_AUTOCONNECT_BLOCKED_REASON_FAILED,
            TRUE);
        nm_policy_device_recheck_auto_activate_schedule(self, device);
        return;
    }

    /* Subscribe to AC state-changed signal to detect when the
     * activation fails in early stages without changing device
     * state.
     */
    if (g_hash_table_add(priv->pending_active_connections, ac)) {
        g_signal_connect(ac,
                         NM_ACTIVE_CONNECTION_STATE_CHANGED,
                         G_CALLBACK(pending_ac_state_changed),
                         g_object_ref(self));
        g_object_weak_ref(G_OBJECT(ac), (GWeakNotify) pending_ac_gone, self);
    }
}

static void
_auto_activate_device_clear(NMPolicy *self, NMDevice *device, gboolean do_activate)
{
    nm_assert(NM_IS_DEVICE(device));
    nm_assert(NM_IS_POLICY(self));
    nm_assert(c_list_is_linked(&device->policy_auto_activate_lst));
    nm_assert(c_list_contains(&NM_POLICY_GET_PRIVATE(self)->policy_auto_activate_lst_head,
                              &device->policy_auto_activate_lst));

    c_list_unlink(&device->policy_auto_activate_lst);
    nm_clear_g_source_inst(&device->policy_auto_activate_idle_source);

    if (do_activate)
        _auto_activate_device(self, device);

    nm_device_remove_pending_action(device, NM_PENDING_ACTION_AUTOACTIVATE, TRUE);
}

static gboolean
_auto_activate_idle_cb(gpointer user_data)
{
    NMDevice *device = user_data;

    nm_assert(NM_IS_DEVICE(device));

    _auto_activate_device_clear(nm_manager_get_policy(nm_device_get_manager(device)), device, TRUE);
    return G_SOURCE_CONTINUE;
}

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

typedef struct {
    NMDevice *device;
    GSList   *secondaries;
} PendingSecondaryData;

static PendingSecondaryData *
pending_secondary_data_new(NMDevice *device, GSList *secondaries)
{
    PendingSecondaryData *data;

    data              = g_slice_new(PendingSecondaryData);
    data->device      = g_object_ref(device);
    data->secondaries = secondaries;
    return data;
}

static void
pending_secondary_data_free(PendingSecondaryData *data)
{
    g_object_unref(data->device);
    g_slist_free_full(data->secondaries, g_object_unref);
    g_slice_free(PendingSecondaryData, data);
}

static void
process_secondaries(NMPolicy *self, NMActiveConnection *active, gboolean connected)
{
    NMPolicyPrivate *priv = NM_POLICY_GET_PRIVATE(self);
    GSList          *iter, *iter2, *next, *next2;

    /* Loop through devices waiting for secondary connections to activate */
    for (iter = priv->pending_secondaries; iter; iter = next) {
        PendingSecondaryData *secondary_data = (PendingSecondaryData *) iter->data;
        NMDevice             *item_device    = secondary_data->device;

        next = g_slist_next(iter);

        /* Look for 'active' in each device's secondary connections list */
        for (iter2 = secondary_data->secondaries; iter2; iter2 = next2) {
            NMActiveConnection *secondary_active = NM_ACTIVE_CONNECTION(iter2->data);

            next2 = g_slist_next(iter2);

            if (active != secondary_active)
                continue;

            if (connected) {
                _LOGD(LOGD_DEVICE,
                      "secondary connection '%s' succeeded; active path '%s'",
                      nm_active_connection_get_settings_connection_id(active),
                      nm_dbus_object_get_path(NM_DBUS_OBJECT(active)));

                /* Secondary connection activated */
                secondary_data->secondaries =
                    g_slist_remove(secondary_data->secondaries, secondary_active);
                g_object_unref(secondary_active);
                if (!secondary_data->secondaries) {
                    /* No secondary UUID remained -> remove the secondary data item */
                    priv->pending_secondaries =
                        g_slist_remove(priv->pending_secondaries, secondary_data);
                    pending_secondary_data_free(secondary_data);
                    if (nm_device_get_state(item_device) == NM_DEVICE_STATE_SECONDARIES)
                        nm_device_state_changed(item_device,
                                                NM_DEVICE_STATE_ACTIVATED,
                                                NM_DEVICE_STATE_REASON_NONE);
                    break;
                }
            } else {
                _LOGD(LOGD_DEVICE,
                      "secondary connection '%s' failed; active path '%s'",
                      nm_active_connection_get_settings_connection_id(active),
                      nm_dbus_object_get_path(NM_DBUS_OBJECT(active)));

                /* Secondary connection failed -> do not watch other connections */
                priv->pending_secondaries =
                    g_slist_remove(priv->pending_secondaries, secondary_data);
                pending_secondary_data_free(secondary_data);
                if (nm_device_get_state(item_device) == NM_DEVICE_STATE_SECONDARIES
                    || nm_device_get_state(item_device) == NM_DEVICE_STATE_ACTIVATED)
                    nm_device_state_changed(item_device,
                                            NM_DEVICE_STATE_FAILED,
                                            NM_DEVICE_STATE_REASON_SECONDARY_CONNECTION_FAILED);
                break;
            }
        }
    }
}

static void
_static_hostname_changed_cb(NMHostnameManager *hostname_manager,
                            GParamSpec        *pspec,
                            gpointer           user_data)
{
    NMPolicyPrivate *priv = user_data;
    NMPolicy        *self = _PRIV_TO_SELF(priv);

    update_system_hostname(self, "hostname changed", FALSE);
}

void
nm_policy_unblock_failed_ovs_interfaces(NMPolicy *self)
{
    NMPolicyPrivate             *priv        = NM_POLICY_GET_PRIVATE(self);
    NMSettingsConnection *const *connections = NULL;
    guint                        i;

    _LOGT(LOGD_DEVICE, "unblocking failed OVS interfaces");

    connections = nm_settings_get_connections(priv->settings, NULL);
    for (i = 0; connections[i]; i++) {
        NMSettingsConnection *sett_conn  = connections[i];
        NMConnection         *connection = nm_settings_connection_get_connection(sett_conn);

        if (nm_connection_get_setting_ovs_interface(connection)) {
            nm_manager_devcon_autoconnect_retries_reset(priv->manager, NULL, sett_conn);
            nm_manager_devcon_autoconnect_blocked_reason_set(
                priv->manager,
                NULL,
                sett_conn,
                NM_SETTINGS_AUTOCONNECT_BLOCKED_REASON_FAILED,
                FALSE);
        }
    }
}

static gboolean
reset_autoconnect_all(
    NMPolicy *self,
    NMDevice *device, /* if present, only reset connections compatible with @device */
    gboolean  only_no_secrets)
{
    NMPolicyPrivate             *priv        = NM_POLICY_GET_PRIVATE(self);
    NMSettingsConnection *const *connections = NULL;
    guint                        i;
    gboolean                     changed = FALSE;

    _LOGD(LOGD_DEVICE,
          "re-enabling autoconnect for all connections%s%s%s",
          device ? " on " : "",
          device ? nm_device_get_iface(device) : "",
          only_no_secrets ? " (only clear no-secrets flag)" : "");

    connections = nm_settings_get_connections(priv->settings, NULL);
    for (i = 0; connections[i]; i++) {
        NMSettingsConnection *sett_conn = connections[i];

        if (device
            && !nm_device_check_connection_compatible(
                device,
                nm_settings_connection_get_connection(sett_conn),
                TRUE,
                NULL))
            continue;

        if (nm_manager_devcon_autoconnect_reset_reconnect_all(priv->manager,
                                                              device,
                                                              sett_conn,
                                                              only_no_secrets))
            changed = TRUE;
    }
    return changed;
}

static void
sleeping_changed(NMManager *manager, GParamSpec *pspec, gpointer user_data)
{
    NMPolicyPrivate *priv     = user_data;
    NMPolicy        *self     = _PRIV_TO_SELF(priv);
    gboolean         sleeping = FALSE, enabled = FALSE;

    g_object_get(G_OBJECT(manager), NM_MANAGER_SLEEPING, &sleeping, NULL);
    g_object_get(G_OBJECT(manager), NM_MANAGER_NETWORKING_ENABLED, &enabled, NULL);

    /* Reset retries on all connections so they'll checked on wakeup */
    if (sleeping || !enabled)
        reset_autoconnect_all(self, NULL, FALSE);
}

void
nm_policy_device_recheck_auto_activate_schedule(NMPolicy *self, NMDevice *device)
{
    NMPolicyPrivate    *priv;
    NMActiveConnection *ac;
    const CList        *tmp_list;

    g_return_if_fail(NM_IS_POLICY(self));
    g_return_if_fail(NM_IS_DEVICE(device));
    nm_assert(g_signal_handler_find(device,
                                    G_SIGNAL_MATCH_DATA,
                                    0,
                                    0,
                                    NULL,
                                    NULL,
                                    NM_POLICY_GET_PRIVATE(self))
              != 0);

    if (!c_list_is_empty(&device->policy_auto_activate_lst)) {
        /* already queued. Return. */
        return;
    }

    priv = NM_POLICY_GET_PRIVATE(self);

    if (nm_manager_get_state(priv->manager) == NM_STATE_ASLEEP)
        return;

    if (!nm_device_autoconnect_allowed(device))
        return;

    nm_manager_for_each_active_connection (priv->manager, ac, tmp_list) {
        if (nm_active_connection_get_device(ac) == device) {
            if (nm_device_managed_type_is_external(device)
                && nm_device_get_allow_autoconnect_on_external(device)) {
                /* pass */
            } else
                return;
        }
    }

    nm_device_add_pending_action(device, NM_PENDING_ACTION_AUTOACTIVATE, TRUE);

    c_list_link_tail(&priv->policy_auto_activate_lst_head, &device->policy_auto_activate_lst);
    device->policy_auto_activate_idle_source = nm_g_idle_add_source(_auto_activate_idle_cb, device);
}

static gboolean
reset_connections_retries(gpointer user_data)
{
    NMPolicy                    *self        = (NMPolicy *) user_data;
    NMPolicyPrivate             *priv        = NM_POLICY_GET_PRIVATE(self);
    NMSettingsConnection *const *connections = NULL;
    guint                        i;
    gint32                       con_stamp, min_stamp, now;
    gboolean                     changed = FALSE;

    nm_clear_g_source_inst(&priv->reset_connections_retries_idle_source);

    min_stamp   = 0;
    now         = nm_utils_get_monotonic_timestamp_sec();
    connections = nm_settings_get_connections(priv->settings, NULL);
    for (i = 0; connections[i]; i++) {
        NMSettingsConnection *connection = connections[i];

        con_stamp =
            nm_manager_devcon_autoconnect_retries_blocked_until(priv->manager, NULL, connection);
        if (con_stamp == 0)
            continue;

        if (con_stamp <= now) {
            nm_manager_devcon_autoconnect_retries_reset(priv->manager, NULL, connection);
            changed = TRUE;
        } else if (min_stamp == 0 || min_stamp > con_stamp)
            min_stamp = con_stamp;
    }

    /* Schedule the handler again if there are some stamps left */
    if (min_stamp != 0) {
        priv->reset_connections_retries_idle_source =
            nm_g_timeout_add_seconds_source(min_stamp - now, reset_connections_retries, self);
    }

    /* If anything changed, try to activate the newly re-enabled connections */
    if (changed)
        nm_policy_device_recheck_auto_activate_all_schedule(self);

    return G_SOURCE_CONTINUE;
}

static void
_connection_autoconnect_retries_set(NMPolicy             *self,
                                    NMDevice             *device,
                                    NMSettingsConnection *connection,
                                    guint32               tries)
{
    NMPolicyPrivate *priv = NM_POLICY_GET_PRIVATE(self);

    nm_assert(NM_IS_SETTINGS_CONNECTION(connection));

    nm_manager_devcon_autoconnect_retries_set(priv->manager, device, connection, tries);

    if (tries == 0) {
        /* Schedule a handler to reset retries count */
        if (!priv->reset_connections_retries_idle_source) {
            gint32 retry_time;

            retry_time = nm_manager_devcon_autoconnect_retries_blocked_until(priv->manager,
                                                                             device,
                                                                             connection);
            nm_assert(retry_time != 0);

            priv->reset_connections_retries_idle_source = nm_g_timeout_add_seconds_source(
                NM_MAX(0, retry_time - nm_utils_get_monotonic_timestamp_sec()),
                reset_connections_retries,
                self);
        }
    }
}

static void
unblock_autoconnect_for_children(NMPolicy   *self,
                                 const char *parent_device,
                                 const char *parent_uuid_settings,
                                 const char *parent_uuid_applied,
                                 const char *parent_mac_addr)
{
    NMPolicyPrivate             *priv = NM_POLICY_GET_PRIVATE(self);
    NMSettingsConnection *const *connections;
    gboolean                     changed;
    guint                        i;

    _LOGT(LOGD_CORE,
          "block-autoconnect: unblocking child profiles for parent ifname=%s%s%s, uuid=%s%s%s"
          "%s%s%s",
          NM_PRINT_FMT_QUOTE_STRING(parent_device),
          NM_PRINT_FMT_QUOTE_STRING(parent_uuid_settings),
          NM_PRINT_FMT_QUOTED(parent_uuid_applied,
                              ", applied-uuid=\"",
                              parent_uuid_applied,
                              "\"",
                              ""));

    changed     = FALSE;
    connections = nm_settings_get_connections(priv->settings, NULL);
    for (i = 0; connections[i]; i++) {
        NMSettingsConnection *sett_conn = connections[i];
        NMConnection         *connection;
        NMDeviceFactory      *factory;
        const char           *parent_name = NULL;

        connection = nm_settings_connection_get_connection(sett_conn);
        factory    = nm_device_factory_manager_find_factory_for_connection(connection);
        if (factory)
            parent_name = nm_device_factory_get_connection_parent(factory, connection);

        if (!parent_name)
            continue;

        if (!NM_IN_STRSET(parent_name,
                          parent_device,
                          parent_uuid_applied,
                          parent_uuid_settings,
                          parent_mac_addr))
            continue;

        if (nm_manager_devcon_autoconnect_retries_reset(priv->manager, NULL, sett_conn))
            changed = TRUE;

        /* unblock the devices associated with that connection */
        if (nm_manager_devcon_autoconnect_blocked_reason_set(
                priv->manager,
                NULL,
                sett_conn,
                NM_SETTINGS_AUTOCONNECT_BLOCKED_REASON_FAILED,
                FALSE)) {
            if (!nm_settings_connection_autoconnect_is_blocked(sett_conn))
                changed = TRUE;
        }
    }

    if (changed)
        nm_policy_device_recheck_auto_activate_all_schedule(self);
}

static void
unblock_autoconnect_for_ports(NMPolicy   *self,
                              const char *controller_device,
                              const char *controller_uuid_settings,
                              const char *controller_uuid_applied)
{
    NMPolicyPrivate             *priv = NM_POLICY_GET_PRIVATE(self);
    NMSettingsConnection *const *connections;
    gboolean                     changed = FALSE;
    guint                        i;

    _LOGT(LOGD_CORE,
          "block-autoconnect: unblocking port profiles for controller ifname=%s%s%s, uuid=%s%s%s"
          "%s%s%s",
          NM_PRINT_FMT_QUOTE_STRING(controller_device),
          NM_PRINT_FMT_QUOTE_STRING(controller_uuid_settings),
          NM_PRINT_FMT_QUOTED(controller_uuid_applied,
                              ", applied-uuid=\"",
                              controller_uuid_applied,
                              "\"",
                              ""));

    connections = nm_settings_get_connections(priv->settings, NULL);
    for (i = 0; connections[i]; i++) {
        NMSettingsConnection *sett_conn = connections[i];
        NMSettingConnection  *s_port_con;
        const char           *port_controller;

        s_port_con = nm_settings_connection_get_setting(sett_conn, NM_META_SETTING_TYPE_CONNECTION);
        port_controller = nm_setting_connection_get_controller(s_port_con);
        if (!port_controller)
            continue;

        if (!NM_IN_STRSET(port_controller,
                          controller_device,
                          controller_uuid_applied,
                          controller_uuid_settings))
            continue;

        if (nm_manager_devcon_autoconnect_retries_reset(priv->manager, NULL, sett_conn))
            changed = TRUE;

        /* unblock the devices associated with that connection */
        if (nm_manager_devcon_autoconnect_blocked_reason_set(
                priv->manager,
                NULL,
                sett_conn,
                NM_SETTINGS_AUTOCONNECT_BLOCKED_REASON_FAILED,
                FALSE)) {
            if (!nm_settings_connection_autoconnect_is_blocked(sett_conn))
                changed = TRUE;
        }
    }

    if (changed)
        nm_policy_device_recheck_auto_activate_all_schedule(self);
}

static void
unblock_autoconnect_for_ports_for_sett_conn(NMPolicy *self, NMSettingsConnection *sett_conn)
{
    const char          *controller_device;
    const char          *controller_uuid_settings;
    NMSettingConnection *s_con;

    nm_assert(NM_IS_POLICY(self));
    nm_assert(NM_IS_SETTINGS_CONNECTION(sett_conn));

    s_con = nm_settings_connection_get_setting(sett_conn, NM_META_SETTING_TYPE_CONNECTION);

    nm_assert(NM_IS_SETTING_CONNECTION(s_con));

    controller_uuid_settings = nm_setting_connection_get_uuid(s_con);
    controller_device        = nm_setting_connection_get_interface_name(s_con);

    unblock_autoconnect_for_ports(self, controller_device, controller_uuid_settings, NULL);
}

static void
activate_port_or_children_connections(NMPolicy *self,
                                      NMDevice *device,
                                      gboolean  activate_children_connections_only)
{
    const char   *controller_device;
    const char   *controller_uuid_settings = NULL;
    const char   *controller_uuid_applied  = NULL;
    const char   *parent_mac_addr          = NULL;
    NMActRequest *req;

    controller_device = nm_device_get_iface(device);
    nm_assert(controller_device);

    parent_mac_addr = nm_device_get_permanent_hw_address(device);

    req = nm_device_get_act_request(device);
    if (req) {
        NMConnection         *connection;
        NMSettingsConnection *sett_conn;

        sett_conn = nm_active_connection_get_settings_connection(NM_ACTIVE_CONNECTION(req));
        if (sett_conn)
            controller_uuid_settings = nm_settings_connection_get_uuid(sett_conn);

        connection = nm_active_connection_get_applied_connection(NM_ACTIVE_CONNECTION(req));
        if (connection)
            controller_uuid_applied = nm_connection_get_uuid(connection);

        if (nm_streq0(controller_uuid_settings, controller_uuid_applied))
            controller_uuid_applied = NULL;
    }

    if (!activate_children_connections_only) {
        unblock_autoconnect_for_ports(self,
                                      controller_device,
                                      controller_uuid_settings,
                                      controller_uuid_applied);
    }
    unblock_autoconnect_for_children(self,
                                     controller_device,
                                     controller_uuid_settings,
                                     controller_uuid_applied,
                                     parent_mac_addr);
}

static gboolean
activate_secondary_connections(NMPolicy *self, NMConnection *connection, NMDevice *device)
{
    NMPolicyPrivate       *priv = NM_POLICY_GET_PRIVATE(self);
    NMSettingConnection   *s_con;
    NMActiveConnection    *ac;
    PendingSecondaryData  *secondary_data;
    GSList                *secondary_ac_list = NULL;
    GError                *error             = NULL;
    guint32                i;
    gboolean               success = TRUE;
    NMActivationStateFlags initial_state_flags;

    s_con = nm_connection_get_setting_connection(connection);
    nm_assert(NM_IS_SETTING_CONNECTION(s_con));

    /* we propagate the activation's state flags. */
    initial_state_flags = nm_device_get_activation_state_flags(device)
                          & NM_ACTIVATION_STATE_FLAG_LIFETIME_BOUND_TO_PROFILE_VISIBILITY;

    for (i = 0; i < nm_setting_connection_get_num_secondaries(s_con); i++) {
        NMSettingsConnection *sett_conn;
        const char           *sec_uuid = nm_setting_connection_get_secondary(s_con, i);
        NMActRequest         *req;

        sett_conn = nm_settings_get_connection_by_uuid(priv->settings, sec_uuid);
        if (!sett_conn) {
            _LOGW(LOGD_DEVICE,
                  "secondary connection '%s' auto-activation failed: The connection doesn't exist.",
                  sec_uuid);
            success = FALSE;
            break;
        }

        if (!nm_connection_is_type(nm_settings_connection_get_connection(sett_conn),
                                   NM_SETTING_VPN_SETTING_NAME)) {
            _LOGW(LOGD_DEVICE,
                  "secondary connection '%s (%s)' auto-activation failed: The connection is not a "
                  "VPN.",
                  nm_settings_connection_get_id(sett_conn),
                  sec_uuid);
            success = FALSE;
            break;
        }

        req = nm_device_get_act_request(device);

        _LOGD(LOGD_DEVICE,
              "activating secondary connection '%s (%s)' for base connection '%s (%s)'",
              nm_settings_connection_get_id(sett_conn),
              sec_uuid,
              nm_connection_get_id(connection),
              nm_connection_get_uuid(connection));
        ac = nm_manager_activate_connection(
            priv->manager,
            sett_conn,
            NULL,
            nm_dbus_object_get_path(NM_DBUS_OBJECT(req)),
            device,
            nm_active_connection_get_subject(NM_ACTIVE_CONNECTION(req)),
            NM_ACTIVATION_TYPE_MANAGED,
            nm_active_connection_get_activation_reason(NM_ACTIVE_CONNECTION(req)),
            initial_state_flags,
            &error);
        if (ac)
            secondary_ac_list = g_slist_append(secondary_ac_list, g_object_ref(ac));
        else {
            _LOGW(LOGD_DEVICE,
                  "secondary connection '%s (%s)' auto-activation failed: (%d) %s",
                  nm_settings_connection_get_id(sett_conn),
                  sec_uuid,
                  error->code,
                  error->message);
            g_clear_error(&error);
            success = FALSE;
            break;
        }
    }

    if (success && secondary_ac_list != NULL) {
        secondary_data            = pending_secondary_data_new(device, secondary_ac_list);
        priv->pending_secondaries = g_slist_append(priv->pending_secondaries, secondary_data);
    } else
        g_slist_free_full(secondary_ac_list, g_object_unref);

    return success;
}

static void
device_state_changed(NMDevice           *device,
                     NMDeviceState       new_state,
                     NMDeviceState       old_state,
                     NMDeviceStateReason reason,
                     gpointer            user_data)
{
    NMPolicyPrivate      *priv = user_data;
    NMPolicy             *self = _PRIV_TO_SELF(priv);
    NMActiveConnection   *ac;
    NMSettingsConnection *sett_conn = nm_device_get_settings_connection(device);
    NMSettingConnection  *s_con     = NULL;

    switch (nm_device_state_reason_check(reason)) {
    case NM_DEVICE_STATE_REASON_GSM_SIM_PIN_REQUIRED:
    case NM_DEVICE_STATE_REASON_GSM_SIM_PUK_REQUIRED:
    case NM_DEVICE_STATE_REASON_SIM_PIN_INCORRECT:
    case NM_DEVICE_STATE_REASON_GSM_APN_FAILED:
        /* Block autoconnection at settings level if there is any settings-specific
         * error reported by the modem (e.g. wrong SIM-PIN or wrong APN). Do not block
         * autoconnection at settings level for errors in the device domain (e.g.
         * a missing SIM or wrong modem initialization).
         */
        if (sett_conn) {
            nm_manager_devcon_autoconnect_blocked_reason_set(
                priv->manager,
                device,
                sett_conn,
                NM_SETTINGS_AUTOCONNECT_BLOCKED_REASON_FAILED,
                TRUE);
        }
        break;
    default:
        break;
    }

    switch (new_state) {
    case NM_DEVICE_STATE_FAILED:
        g_signal_handlers_disconnect_by_func(device, device_dns_lookup_done, self);

        /* Mark the connection invalid if it failed during activation so that
         * it doesn't get automatically chosen over and over and over again.
         */
        if (sett_conn && old_state >= NM_DEVICE_STATE_PREPARE
            && old_state <= NM_DEVICE_STATE_ACTIVATED) {
            gboolean blocked = FALSE;
            guint64  con_v;

            switch (nm_device_state_reason_check(reason)) {
            case NM_DEVICE_STATE_REASON_NO_SECRETS:
                /* we want to block the connection from auto-connect if it failed due to no-secrets.
                 * However, if a secret-agent registered, since the connection made the last
                 * secret-request, we do not block it. The new secret-agent might not yet
                 * been consulted, and it may be able to provide the secrets.
                 *
                 * We detect this by using a version-id of the agent-manager, which increments
                 * whenever new agents register. Note that the agent-manager's version-id is
                 * never zero and strictly increasing.
                 *
                 * A connection's version-id of zero means that the connection never tried to request secrets.
                 * That can happen when nm_settings_connection_get_secrets() fails early without actually
                 * consulting any agents.
                 */
                con_v = nm_settings_connection_get_last_secret_agent_version_id(sett_conn);
                if (con_v == 0 || con_v == nm_agent_manager_get_agent_version_id(priv->agent_mgr)) {
                    _LOGD(LOGD_DEVICE,
                          "block-autoconnect: connection '%s' now blocked from autoconnect due to "
                          "no secrets",
                          nm_settings_connection_get_id(sett_conn));
                    nm_settings_connection_autoconnect_blocked_reason_set(
                        sett_conn,
                        NM_SETTINGS_AUTOCONNECT_BLOCKED_REASON_NO_SECRETS,
                        TRUE);
                    blocked = TRUE;
                }
                break;
            case NM_DEVICE_STATE_REASON_DEPENDENCY_FAILED:
                /* A connection that fails due to dependency-failed is not able to
                 * reconnect until the connection it depends on activates again;
                 * when this happens, the controller or parent clears the blocked
                 * reason for all its dependent devices in activate_port_or_children_connections()
                 * and tries to reconnect them. For this to work, the port should
                 * be marked as blocked when it fails with dependency-failed.
                 */
                _LOGD(LOGD_DEVICE,
                      "block-autoconnect: connection[%p] (%s) now blocked from autoconnect due to "
                      "failed "
                      "dependency",
                      sett_conn,
                      nm_settings_connection_get_id(sett_conn));
                nm_manager_devcon_autoconnect_blocked_reason_set(
                    priv->manager,
                    device,
                    sett_conn,
                    NM_SETTINGS_AUTOCONNECT_BLOCKED_REASON_FAILED,
                    TRUE);
                blocked = TRUE;
                break;
            default:
                break;
            }

            if (!blocked) {
                guint32 tries;

                tries = nm_manager_devcon_autoconnect_retries_get(priv->manager, device, sett_conn);
                if (tries == 0) {
                    /* blocked */
                } else if (tries != NM_AUTOCONNECT_RETRIES_FOREVER) {
                    _LOGD(LOGD_DEVICE,
                          "autoconnect: connection[%p] (%s): failed to autoconnect; %u tries left",
                          sett_conn,
                          nm_settings_connection_get_id(sett_conn),
                          tries - 1u);
                    _connection_autoconnect_retries_set(self, device, sett_conn, tries - 1u);
                } else {
                    _LOGD(LOGD_DEVICE,
                          "autoconnect: connection[%p] (%s) failed to autoconnect; infinite tries "
                          "left",
                          sett_conn,
                          nm_settings_connection_get_id(sett_conn));
                }
            }
        }
        break;
    case NM_DEVICE_STATE_ACTIVATED:
        if (nm_device_get_device_type(device) == NM_DEVICE_TYPE_OVS_INTERFACE) {
            /* When parent is ovs-interface, the kernel link is only created in stage3, we have to
            * delay unblocking the children and schedule them for activation until parent is activated */
            activate_port_or_children_connections(self, device, TRUE);
        }
        if (sett_conn) {
            /* Reset auto retries back to default since connection was successful */
            nm_manager_devcon_autoconnect_retries_reset(priv->manager, device, sett_conn);
        }

        /* Since there is no guarantee that device_l3cd_changed() is called
         * again when the device becomes ACTIVATED, we need also to update
         * routing and DNS here. */
        nm_dns_manager_begin_updates(priv->dns_manager, __func__);
        if (!nm_device_managed_type_is_external(device)) {
            nm_dns_manager_set_ip_config(priv->dns_manager,
                                         AF_UNSPEC,
                                         device,
                                         nm_device_get_l3cd(device, TRUE),
                                         nm_device_is_vpn(device) ? NM_DNS_IP_CONFIG_TYPE_VPN
                                                                  : NM_DNS_IP_CONFIG_TYPE_DEFAULT,
                                         TRUE);
        }
        update_ip_dns(self, AF_INET, device);
        update_ip_dns(self, AF_INET6, device);
        update_ip4_routing(self, TRUE);
        update_ip6_routing(self, TRUE);
        update_system_hostname(self, "routing and dns", TRUE);
        nm_dns_manager_end_updates(priv->dns_manager, __func__);

        break;
    case NM_DEVICE_STATE_UNMANAGED:
    case NM_DEVICE_STATE_UNAVAILABLE:
        if (old_state > NM_DEVICE_STATE_DISCONNECTED)
            update_routing_and_dns(self, FALSE, device);
        break;
    case NM_DEVICE_STATE_DEACTIVATING:
        if (sett_conn) {
            NMSettingsAutoconnectBlockedReason blocked_reason =
                NM_SETTINGS_AUTOCONNECT_BLOCKED_REASON_NONE;

            switch (nm_device_state_reason_check(reason)) {
            case NM_DEVICE_STATE_REASON_USER_REQUESTED:
                blocked_reason = NM_SETTINGS_AUTOCONNECT_BLOCKED_REASON_USER_REQUEST;
                break;
            case NM_DEVICE_STATE_REASON_DEPENDENCY_FAILED:
                blocked_reason = NM_SETTINGS_AUTOCONNECT_BLOCKED_REASON_FAILED;
                break;
            default:
                break;
            }
            if (blocked_reason != NM_SETTINGS_AUTOCONNECT_BLOCKED_REASON_NONE) {
                _LOGD(LOGD_DEVICE,
                      "block-autoconnect: blocking autoconnect of connection '%s': %s",
                      nm_settings_connection_get_id(sett_conn),
                      NM_UTILS_LOOKUP_STR_A(nm_device_state_reason_to_string,
                                            nm_device_state_reason_check(reason)));
                if (blocked_reason == NM_SETTINGS_AUTOCONNECT_BLOCKED_REASON_FAILED)
                    nm_manager_devcon_autoconnect_blocked_reason_set(priv->manager,
                                                                     device,
                                                                     sett_conn,
                                                                     blocked_reason,
                                                                     TRUE);
                else
                    nm_settings_connection_autoconnect_blocked_reason_set(sett_conn,
                                                                          blocked_reason,
                                                                          TRUE);
            }
        }
        ip6_remove_device_prefix_delegations(self, device);
        break;
    case NM_DEVICE_STATE_DISCONNECTED:
        g_signal_handlers_disconnect_by_func(device, device_dns_lookup_done, self);

        /* Reset retry counts for a device's connections when carrier on; if cable
         * was unplugged and plugged in again, we should try to reconnect.
         */
        if (nm_device_state_reason_check(reason) == NM_DEVICE_STATE_REASON_CARRIER
            && old_state == NM_DEVICE_STATE_UNAVAILABLE)
            reset_autoconnect_all(self, device, FALSE);

        if (old_state > NM_DEVICE_STATE_DISCONNECTED)
            update_routing_and_dns(self, FALSE, device);

        /* Device is now available for auto-activation */
        nm_policy_device_recheck_auto_activate_schedule(self, device);
        break;

    case NM_DEVICE_STATE_PREPARE:
        /* Reset auto-connect retries of all ports or children and schedule them for
         * activation. */
        activate_port_or_children_connections(self, device, FALSE);

        /* Now that the device state is progressing, we don't care
         * anymore for the AC state. */
        ac = (NMActiveConnection *) nm_device_get_act_request(device);
        if (ac && g_hash_table_remove(priv->pending_active_connections, ac)) {
            g_signal_handlers_disconnect_by_func(ac, pending_ac_state_changed, self);
            g_object_weak_unref(G_OBJECT(ac), pending_ac_gone, self);
            g_object_unref(self);
        }
        break;
    case NM_DEVICE_STATE_IP_CONFIG:
        /* We must have secrets if we got here. */
        if (sett_conn)
            nm_manager_devcon_autoconnect_blocked_reason_set(
                priv->manager,
                device,
                sett_conn,
                NM_SETTINGS_AUTOCONNECT_BLOCKED_REASON_FAILED,
                FALSE);
        break;
    case NM_DEVICE_STATE_SECONDARIES:
        if (sett_conn)
            s_con = nm_connection_get_setting_connection(
                nm_settings_connection_get_connection(sett_conn));
        if (s_con && nm_setting_connection_get_num_secondaries(s_con) > 0) {
            /* Make routes and DNS up-to-date before activating dependent connections */
            update_routing_and_dns(self, FALSE, device);

            /* Activate secondary (VPN) connections */
            if (!activate_secondary_connections(self,
                                                nm_settings_connection_get_connection(sett_conn),
                                                device)) {
                nm_device_queue_state(device,
                                      NM_DEVICE_STATE_FAILED,
                                      NM_DEVICE_STATE_REASON_SECONDARY_CONNECTION_FAILED);
            }
        } else
            nm_device_queue_state(device, NM_DEVICE_STATE_ACTIVATED, NM_DEVICE_STATE_REASON_NONE);
        break;

    default:
        break;
    }

    check_activating_active_connections(self);
}

static void
device_l3cd_changed(NMDevice             *device,
                    const NML3ConfigData *l3cd_old,
                    const NML3ConfigData *l3cd_new,
                    gpointer              user_data)
{
    NMPolicyPrivate *priv = user_data;
    NMPolicy        *self = _PRIV_TO_SELF(priv);
    NMDeviceState    state;

    nm_assert(!l3cd_new || NM_IS_L3_CONFIG_DATA(l3cd_new));
    nm_assert(!l3cd_old || NM_IS_L3_CONFIG_DATA(l3cd_old));

    nm_dns_manager_begin_updates(priv->dns_manager, __func__);

    /* FIXME(l3cfg): Note that this function is not always called when the
     * device becomes ACTIVATED. Previously, we would also update the DNS
     * manager's IP config in device_state_change(ACTIVATED). There we would
     * also special-case pseudo-VPNs like wireguard. I don't see the code where
     * this is handled now.
     */
    state = nm_device_get_state(device);
    if (l3cd_new && state >= NM_DEVICE_STATE_IP_CONFIG && state < NM_DEVICE_STATE_DEACTIVATING) {
        /* Since the device L3CD_CHANGED signal is emitted *after* the commit of
         * configuration, addresses and routes are already set in kernel when we
         * write the configuration to resolv.conf or send it to the DNS plugin.
         * This prevents "leaks" of DNS queries via the wrong routes.*/
        nm_dns_manager_set_ip_config(priv->dns_manager,
                                     AF_UNSPEC,
                                     device,
                                     l3cd_new,
                                     nm_device_is_vpn(device) ? NM_DNS_IP_CONFIG_TYPE_VPN
                                                              : NM_DNS_IP_CONFIG_TYPE_DEFAULT,
                                     TRUE);
        update_ip_dns(self, AF_INET, device);
        update_ip_dns(self, AF_INET6, device);
        update_ip4_routing(self, TRUE);
        update_ip6_routing(self, TRUE);
        /* FIXME: since we already monitor platform addresses changes,
         * this is probably no longer necessary? */
        update_system_hostname(self, "ip conf", FALSE);
    } else {
        nm_dns_manager_set_ip_config(priv->dns_manager,
                                     AF_UNSPEC,
                                     device,
                                     l3cd_old,
                                     NM_DNS_IP_CONFIG_TYPE_REMOVED,
                                     TRUE);
    }

    nm_dns_manager_end_updates(priv->dns_manager, __func__);
}

static void
device_platform_address_changed(NMDevice *device, gpointer user_data)
{
    NMPolicyPrivate *priv = user_data;
    NMPolicy        *self = _PRIV_TO_SELF(priv);
    NMDeviceState    state;

    state = nm_device_get_state(device);
    if (state > NM_DEVICE_STATE_DISCONNECTED && state < NM_DEVICE_STATE_DEACTIVATING) {
        update_system_hostname(self, "address changed", TRUE);
    }
}

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

static void
device_autoconnect_changed(NMDevice *device, GParamSpec *pspec, gpointer user_data)
{
    NMPolicyPrivate *priv = user_data;
    NMPolicy        *self = _PRIV_TO_SELF(priv);

    nm_policy_device_recheck_auto_activate_schedule(self, device);
}

static void
devices_list_unregister(NMPolicy *self, NMDevice *device)
{
    NMPolicyPrivate *priv = NM_POLICY_GET_PRIVATE(self);

    g_signal_handlers_disconnect_by_data((GObject *) device, priv);
}

static void
devices_list_register(NMPolicy *self, NMDevice *device)
{
    NMPolicyPrivate *priv = NM_POLICY_GET_PRIVATE(self);

    /* Connect state-changed with _after, so that the handler is invoked after other handlers. */
    g_signal_connect_after(device, NM_DEVICE_STATE_CHANGED, G_CALLBACK(device_state_changed), priv);
    g_signal_connect(device, NM_DEVICE_L3CD_CHANGED, G_CALLBACK(device_l3cd_changed), priv);
    g_signal_connect(device,
                     NM_DEVICE_PLATFORM_ADDRESS_CHANGED,
                     G_CALLBACK(device_platform_address_changed),
                     priv);
    g_signal_connect(device,
                     NM_DEVICE_IP6_PREFIX_DELEGATED,
                     G_CALLBACK(device_ip6_prefix_delegated),
                     priv);
    g_signal_connect(device,
                     NM_DEVICE_IP6_SUBNET_NEEDED,
                     G_CALLBACK(device_ip6_subnet_needed),
                     priv);
    g_signal_connect(device,
                     "notify::" NM_DEVICE_AUTOCONNECT,
                     G_CALLBACK(device_autoconnect_changed),
                     priv);
}

static void
device_added(NMManager *manager, NMDevice *device, gpointer user_data)
{
    NMPolicyPrivate *priv = user_data;
    NMPolicy        *self = _PRIV_TO_SELF(priv);

    g_return_if_fail(NM_IS_POLICY(self));

    priv = NM_POLICY_GET_PRIVATE(self);

    if (!g_hash_table_add(priv->devices, device))
        g_return_if_reached();

    devices_list_register(self, device);
}

static void
device_removed(NMManager *manager, NMDevice *device, gpointer user_data)
{
    NMPolicyPrivate *priv = user_data;
    NMPolicy        *self = _PRIV_TO_SELF(priv);

    /* TODO: is this needed? The delegations are cleaned up
     * on transition to deactivated too. */
    ip6_remove_device_prefix_delegations(self, device);

    if (c_list_is_linked(&device->policy_auto_activate_lst))
        _auto_activate_device_clear(self, device, FALSE);

    if (g_hash_table_remove(priv->devices, device))
        devices_list_unregister(self, device);

    /* Don't update routing and DNS here as we've already handled that
     * for devices that need it when the device's state changed to UNMANAGED.
     */
}

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

static void
vpn_connection_update_dns(NMPolicy *self, NMVpnConnection *vpn, gboolean remove)
{
    NMPolicyPrivate *priv = NM_POLICY_GET_PRIVATE(self);
    int              IS_IPv4;

    nm_dns_manager_begin_updates(priv->dns_manager, __func__);

    for (IS_IPv4 = 1; IS_IPv4 >= 0; IS_IPv4--) {
        int                   addr_family = IS_IPv4 ? AF_INET : AF_INET6;
        const NML3ConfigData *l3cd;

        l3cd = nm_vpn_connection_get_l3cd(vpn);
        nm_dns_manager_set_ip_config(priv->dns_manager,
                                     addr_family,
                                     vpn,
                                     l3cd,
                                     remove ? NM_DNS_IP_CONFIG_TYPE_REMOVED
                                            : NM_DNS_IP_CONFIG_TYPE_VPN,
                                     TRUE);
    }

    update_routing_and_dns(self, TRUE, NULL);

    nm_dns_manager_end_updates(priv->dns_manager, __func__);
}

static void
vpn_connection_state_changed(NMVpnConnection              *vpn,
                             NMVpnConnectionState          new_state,
                             NMVpnConnectionState          old_state,
                             NMActiveConnectionStateReason reason,
                             NMPolicy                     *self)
{
    /* FIXME(l3cfg): we need to track changes to nm_vpn_connection_get_l3cd(). */
    if (new_state == NM_VPN_CONNECTION_STATE_ACTIVATED)
        vpn_connection_update_dns(self, vpn, FALSE);
    else if (new_state >= NM_VPN_CONNECTION_STATE_FAILED) {
        /* Only clean up IP/DNS if the connection ever got past IP_CONFIG */
        if (old_state >= NM_VPN_CONNECTION_STATE_IP_CONFIG_GET
            && old_state <= NM_VPN_CONNECTION_STATE_ACTIVATED)
            vpn_connection_update_dns(self, vpn, TRUE);
    }
}

static void
vpn_connection_retry_after_failure(NMVpnConnection *vpn, NMPolicy *self)
{
    NMPolicyPrivate      *priv       = NM_POLICY_GET_PRIVATE(self);
    NMActiveConnection   *ac         = NM_ACTIVE_CONNECTION(vpn);
    NMSettingsConnection *connection = nm_active_connection_get_settings_connection(ac);
    GError               *error      = NULL;

    /* Attempt to reconnect VPN connections that failed after being connected */
    if (!nm_manager_activate_connection(
            priv->manager,
            connection,
            NULL,
            NULL,
            NULL,
            nm_active_connection_get_subject(ac),
            NM_ACTIVATION_TYPE_MANAGED,
            nm_active_connection_get_activation_reason(ac),
            (nm_active_connection_get_state_flags(ac)
             & NM_ACTIVATION_STATE_FLAG_LIFETIME_BOUND_TO_PROFILE_VISIBILITY),
            &error)) {
        _LOGW(LOGD_DEVICE,
              "VPN '%s' reconnect failed: %s",
              nm_settings_connection_get_id(connection),
              error->message ?: "unknown");
        g_clear_error(&error);
    }
}

static void
active_connection_state_changed(NMActiveConnection *active, GParamSpec *pspec, NMPolicy *self)
{
    NMActiveConnectionState state = nm_active_connection_get_state(active);

    if (state == NM_ACTIVE_CONNECTION_STATE_ACTIVATED)
        process_secondaries(self, active, TRUE);
    else if (state == NM_ACTIVE_CONNECTION_STATE_DEACTIVATED)
        process_secondaries(self, active, FALSE);
}

static void
active_connection_keep_alive_changed(NMKeepAlive *keep_alive, GParamSpec *pspec, NMPolicy *self)
{
    NMPolicyPrivate    *priv;
    NMActiveConnection *ac;
    GError             *error = NULL;

    nm_assert(NM_IS_POLICY(self));
    nm_assert(NM_IS_KEEP_ALIVE(keep_alive));
    nm_assert(NM_IS_ACTIVE_CONNECTION(nm_keep_alive_get_owner(keep_alive)));

    if (nm_keep_alive_is_alive(keep_alive))
        return;

    ac = nm_keep_alive_get_owner(keep_alive);

    if (nm_active_connection_get_state(ac) > NM_ACTIVE_CONNECTION_STATE_ACTIVATED)
        return;

    priv = NM_POLICY_GET_PRIVATE(self);

    if (!nm_manager_deactivate_connection(priv->manager,
                                          ac,
                                          NM_DEVICE_STATE_REASON_CONNECTION_REMOVED,
                                          &error)) {
        _LOGW(LOGD_DEVICE,
              "connection '%s' is no longer kept alive, but error deactivating it: %s",
              nm_active_connection_get_settings_connection_id(ac),
              error->message);
        g_clear_error(&error);
    }
}

static void
active_connection_added(NMManager *manager, NMActiveConnection *active, gpointer user_data)
{
    NMPolicyPrivate *priv = user_data;
    NMPolicy        *self = _PRIV_TO_SELF(priv);
    NMKeepAlive     *keep_alive;

    if (NM_IS_VPN_CONNECTION(active)) {
        g_signal_connect(active,
                         NM_VPN_CONNECTION_INTERNAL_STATE_CHANGED,
                         G_CALLBACK(vpn_connection_state_changed),
                         self);
        g_signal_connect(active,
                         NM_VPN_CONNECTION_INTERNAL_RETRY_AFTER_FAILURE,
                         G_CALLBACK(vpn_connection_retry_after_failure),
                         self);
    }

    keep_alive = nm_active_connection_get_keep_alive(active);

    nm_keep_alive_arm(keep_alive);

    g_signal_connect(active,
                     "notify::" NM_ACTIVE_CONNECTION_STATE,
                     G_CALLBACK(active_connection_state_changed),
                     self);
    g_signal_connect(keep_alive,
                     "notify::" NM_KEEP_ALIVE_ALIVE,
                     G_CALLBACK(active_connection_keep_alive_changed),
                     self);
    active_connection_keep_alive_changed(keep_alive, NULL, self);
}

static void
active_connection_removed(NMManager *manager, NMActiveConnection *active, gpointer user_data)
{
    NMPolicyPrivate *priv = user_data;
    NMPolicy        *self = _PRIV_TO_SELF(priv);

    g_signal_handlers_disconnect_by_func(active, vpn_connection_state_changed, self);
    g_signal_handlers_disconnect_by_func(active, vpn_connection_retry_after_failure, self);
    g_signal_handlers_disconnect_by_func(active, active_connection_state_changed, self);
    g_signal_handlers_disconnect_by_func(nm_active_connection_get_keep_alive(active),
                                         active_connection_keep_alive_changed,
                                         self);
}

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

static gboolean
_device_recheck_auto_activate_all_cb(gpointer user_data)
{
    NMPolicy        *self = user_data;
    NMPolicyPrivate *priv = NM_POLICY_GET_PRIVATE(self);
    const CList     *tmp_lst;
    NMDevice        *device;

    nm_clear_g_source_inst(&priv->device_recheck_auto_activate_all_idle_source);

    nm_manager_for_each_device (priv->manager, device, tmp_lst)
        nm_policy_device_recheck_auto_activate_schedule(self, device);

    return G_SOURCE_CONTINUE;
}

static void
nm_policy_device_recheck_auto_activate_all_schedule(NMPolicy *self)
{
    NMPolicyPrivate *priv = NM_POLICY_GET_PRIVATE(self);

    /* always restart the idle handler. That way, we settle
     * all other events before restarting to activate them. */
    nm_clear_g_source_inst(&priv->device_recheck_auto_activate_all_idle_source);

    priv->device_recheck_auto_activate_all_idle_source =
        nm_g_idle_add_source(_device_recheck_auto_activate_all_cb, self);
}

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

static void
connection_added(NMSettings *settings, NMSettingsConnection *connection, gpointer user_data)
{
    NMPolicyPrivate *priv = user_data;
    NMPolicy        *self = _PRIV_TO_SELF(priv);

    unblock_autoconnect_for_ports_for_sett_conn(self, connection);

    nm_policy_device_recheck_auto_activate_all_schedule(self);
}

static void
firewall_state_changed(NMFirewalldManager *manager, int signal_type_i, gpointer user_data)
{
    const NMFirewalldManagerStateChangedType signal_type = signal_type_i;
    NMPolicy                                *self        = user_data;
    NMPolicyPrivate                         *priv        = NM_POLICY_GET_PRIVATE(self);
    const CList                             *tmp_lst;
    NMDevice                                *device;

    if (signal_type == NM_FIREWALLD_MANAGER_STATE_CHANGED_TYPE_INITIALIZED) {
        /* the firewall manager was initializing, but all requests
         * so fare were queued and are already sent. No need to
         * re-update the firewall zone of the devices. */
        return;
    }

    if (!nm_firewalld_manager_get_running(manager))
        return;

    /* add interface of each device to correct zone */
    nm_manager_for_each_device (priv->manager, device, tmp_lst)
        nm_device_update_firewall_zone(device);
}

static void
dns_config_changed(NMDnsManager *dns_manager, gpointer user_data)
{
    NMPolicy        *self = (NMPolicy *) user_data;
    NMPolicyPrivate *priv = NM_POLICY_GET_PRIVATE(self);
    NMDevice        *device;
    const CList     *tmp_lst;

    /* We are currently updating the hostname in the DNS manager.
     * This doesn't warrant a new DNS lookup.*/
    if (priv->updating_dns)
        return;

    if (!nm_dns_manager_is_unmanaged(dns_manager)) {
        nm_manager_for_each_device (priv->manager, device, tmp_lst) {
            nm_device_clear_dns_lookup_data(device, "DNS configuration changed");
        }

        update_system_hostname(self, "DNS configuration changed", FALSE);
    }

    nm_dispatcher_call_dns_change();
}

static void
connection_updated(NMSettings           *settings,
                   NMSettingsConnection *connection,
                   guint                 update_reason_u,
                   gpointer              user_data)
{
    NMPolicyPrivate                 *priv          = user_data;
    NMPolicy                        *self          = _PRIV_TO_SELF(priv);
    NMSettingsConnectionUpdateReason update_reason = update_reason_u;

    unblock_autoconnect_for_ports_for_sett_conn(self, connection);

    if (NM_FLAGS_HAS(update_reason, NM_SETTINGS_CONNECTION_UPDATE_REASON_REAPPLY_PARTIAL)) {
        const CList *tmp_lst;
        NMDevice    *device;

        /* find device with given connection */
        nm_manager_for_each_device (priv->manager, device, tmp_lst) {
            if (nm_device_get_settings_connection(device) == connection)
                nm_device_reapply_settings_immediately(device);
        }
    }

    nm_policy_device_recheck_auto_activate_all_schedule(self);
}

static void
connection_removed(NMSettings *settings, NMSettingsConnection *connection, gpointer user_data)
{
    NMPolicyPrivate *priv = user_data;

    nm_manager_deactivate_ac(priv->manager, connection);
}

static void
connection_flags_changed(NMSettings *settings, NMSettingsConnection *connection, gpointer user_data)
{
    NMPolicyPrivate *priv = user_data;
    NMPolicy        *self = _PRIV_TO_SELF(priv);

    if (NM_FLAGS_HAS(nm_settings_connection_get_flags(connection),
                     NM_SETTINGS_CONNECTION_INT_FLAGS_VISIBLE)) {
        if (!nm_settings_connection_autoconnect_is_blocked(connection))
            nm_policy_device_recheck_auto_activate_all_schedule(self);
    }
}

static void
secret_agent_registered(NMSettings *settings, NMSecretAgent *agent, gpointer user_data)
{
    NMPolicy *self = NM_POLICY(user_data);

    /* The registered secret agent may provide some missing secrets. Thus we
     * reset retries count here and schedule activation, so that the
     * connections failed due to missing secrets may re-try auto-connection.
     */
    if (reset_autoconnect_all(self, NULL, TRUE))
        nm_policy_device_recheck_auto_activate_all_schedule(self);
}

NMActiveConnection *
nm_policy_get_default_ip4_ac(NMPolicy *self)
{
    return NM_POLICY_GET_PRIVATE(self)->default_ac4;
}

NMActiveConnection *
nm_policy_get_default_ip6_ac(NMPolicy *self)
{
    return NM_POLICY_GET_PRIVATE(self)->default_ac6;
}

NMActiveConnection *
nm_policy_get_activating_ip4_ac(NMPolicy *self)
{
    return NM_POLICY_GET_PRIVATE(self)->activating_ac4;
}

NMActiveConnection *
nm_policy_get_activating_ip6_ac(NMPolicy *self)
{
    return NM_POLICY_GET_PRIVATE(self)->activating_ac6;
}

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

static NM_UTILS_LOOKUP_STR_DEFINE(_hostname_mode_to_string,
                                  NMPolicyHostnameMode,
                                  NM_UTILS_LOOKUP_DEFAULT_NM_ASSERT("unknown"),
                                  NM_UTILS_LOOKUP_STR_ITEM(NM_POLICY_HOSTNAME_MODE_NONE, "none"),
                                  NM_UTILS_LOOKUP_STR_ITEM(NM_POLICY_HOSTNAME_MODE_DHCP, "dhcp"),
                                  NM_UTILS_LOOKUP_STR_ITEM(NM_POLICY_HOSTNAME_MODE_FULL, "full"), );

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

static void
get_property(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec)
{
    NMPolicy        *self = NM_POLICY(object);
    NMPolicyPrivate *priv = NM_POLICY_GET_PRIVATE(self);

    switch (prop_id) {
    case PROP_DEFAULT_IP4_AC:
        g_value_set_object(value, priv->default_ac4);
        break;
    case PROP_DEFAULT_IP6_AC:
        g_value_set_object(value, priv->default_ac6);
        break;
    case PROP_ACTIVATING_IP4_AC:
        g_value_set_object(value, priv->activating_ac4);
        break;
    case PROP_ACTIVATING_IP6_AC:
        g_value_set_object(value, priv->activating_ac6);
        break;
    default:
        G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
        break;
    }
}

static void
set_property(GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec)
{
    NMPolicy        *self = NM_POLICY(object);
    NMPolicyPrivate *priv = NM_POLICY_GET_PRIVATE(self);

    switch (prop_id) {
    case PROP_MANAGER:
        /* construct-only */
        priv->manager = g_value_get_object(value);
        g_return_if_fail(NM_IS_MANAGER(priv->manager));
        break;
    case PROP_SETTINGS:
        /* construct-only */
        priv->settings = g_value_dup_object(value);
        g_return_if_fail(NM_IS_SETTINGS(priv->settings));
        break;
    default:
        G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
        break;
    }
}

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

static void
nm_policy_init(NMPolicy *self)
{
    NMPolicyPrivate *priv          = NM_POLICY_GET_PRIVATE(self);
    gs_free char    *hostname_mode = NULL;

    c_list_init(&priv->policy_auto_activate_lst_head);

    priv->netns = g_object_ref(nm_netns_get());

    priv->hostname_manager = g_object_ref(nm_hostname_manager_get());

    hostname_mode =
        nm_config_data_get_value(NM_CONFIG_GET_DATA_ORIG,
                                 NM_CONFIG_KEYFILE_GROUP_MAIN,
                                 NM_CONFIG_KEYFILE_KEY_MAIN_HOSTNAME_MODE,
                                 NM_CONFIG_GET_VALUE_STRIP | NM_CONFIG_GET_VALUE_NO_EMPTY);
    if (nm_streq0(hostname_mode, "none"))
        priv->hostname_mode = NM_POLICY_HOSTNAME_MODE_NONE;
    else if (nm_streq0(hostname_mode, "dhcp"))
        priv->hostname_mode = NM_POLICY_HOSTNAME_MODE_DHCP;
    else /* default - full mode */
        priv->hostname_mode = NM_POLICY_HOSTNAME_MODE_FULL;

    priv->devices                    = g_hash_table_new(nm_direct_hash, NULL);
    priv->pending_active_connections = g_hash_table_new(nm_direct_hash, NULL);
    priv->ip6_prefix_delegations     = g_array_new(FALSE, FALSE, sizeof(IP6PrefixDelegation));
    g_array_set_clear_func(priv->ip6_prefix_delegations, clear_ip6_prefix_delegation);
}

static void
constructed(GObject *object)
{
    NMPolicy        *self     = NM_POLICY(object);
    NMPolicyPrivate *priv     = NM_POLICY_GET_PRIVATE(self);
    char            *hostname = NULL;

    /* Grab hostname on startup and use that if nothing provides one */
    if ((hostname = _get_hostname(self))) {
        /* init last_hostname */
        priv->last_hostname = hostname;

        /* only cache it if it's a valid hostname */
        if (nm_utils_is_specific_hostname(hostname))
            priv->orig_hostname = g_strdup(hostname);
    }
    _LOGT(LOGD_DNS,
          "hostname-original: set to %s%s%s",
          NM_PRINT_FMT_QUOTE_STRING(priv->orig_hostname));

    priv->agent_mgr = g_object_ref(nm_agent_manager_get());

    priv->firewalld_manager = g_object_ref(nm_firewalld_manager_get());
    g_signal_connect(priv->firewalld_manager,
                     NM_FIREWALLD_MANAGER_STATE_CHANGED,
                     G_CALLBACK(firewall_state_changed),
                     self);

    priv->dns_manager = g_object_ref(nm_dns_manager_get());
    nm_dns_manager_set_hostname(priv->dns_manager, priv->orig_hostname, TRUE);
    priv->config_changed_id = g_signal_connect(priv->dns_manager,
                                               NM_DNS_MANAGER_CONFIG_CHANGED,
                                               G_CALLBACK(dns_config_changed),
                                               self);

    g_signal_connect(priv->hostname_manager,
                     "notify::" NM_HOSTNAME_MANAGER_STATIC_HOSTNAME,
                     G_CALLBACK(_static_hostname_changed_cb),
                     priv);

    g_signal_connect(priv->manager,
                     "notify::" NM_MANAGER_SLEEPING,
                     G_CALLBACK(sleeping_changed),
                     priv);
    g_signal_connect(priv->manager,
                     "notify::" NM_MANAGER_NETWORKING_ENABLED,
                     G_CALLBACK(sleeping_changed),
                     priv);
    g_signal_connect(priv->manager,
                     NM_MANAGER_INTERNAL_DEVICE_ADDED,
                     G_CALLBACK(device_added),
                     priv);
    g_signal_connect(priv->manager,
                     NM_MANAGER_INTERNAL_DEVICE_REMOVED,
                     G_CALLBACK(device_removed),
                     priv);
    g_signal_connect(priv->manager,
                     NM_MANAGER_ACTIVE_CONNECTION_ADDED,
                     G_CALLBACK(active_connection_added),
                     priv);
    g_signal_connect(priv->manager,
                     NM_MANAGER_ACTIVE_CONNECTION_REMOVED,
                     G_CALLBACK(active_connection_removed),
                     priv);

    g_signal_connect(priv->settings,
                     NM_SETTINGS_SIGNAL_CONNECTION_ADDED,
                     G_CALLBACK(connection_added),
                     priv);
    g_signal_connect(priv->settings,
                     NM_SETTINGS_SIGNAL_CONNECTION_UPDATED,
                     G_CALLBACK(connection_updated),
                     priv);
    g_signal_connect(priv->settings,
                     NM_SETTINGS_SIGNAL_CONNECTION_REMOVED,
                     G_CALLBACK(connection_removed),
                     priv);
    g_signal_connect(priv->settings,
                     NM_SETTINGS_SIGNAL_CONNECTION_FLAGS_CHANGED,
                     G_CALLBACK(connection_flags_changed),
                     priv);

    g_signal_connect(priv->agent_mgr,
                     NM_AGENT_MANAGER_AGENT_REGISTERED,
                     G_CALLBACK(secret_agent_registered),
                     self);

    G_OBJECT_CLASS(nm_policy_parent_class)->constructed(object);

    _LOGD(LOGD_DNS, "hostname-mode: %s", _hostname_mode_to_string(priv->hostname_mode));
    update_system_hostname(self, "initial hostname", FALSE);
}

NMPolicy *
nm_policy_new(NMManager *manager, NMSettings *settings)
{
    g_return_val_if_fail(NM_IS_MANAGER(manager), NULL);
    g_return_val_if_fail(NM_IS_SETTINGS(settings), NULL);

    return g_object_new(NM_TYPE_POLICY,
                        NM_POLICY_MANAGER,
                        manager,
                        NM_POLICY_SETTINGS,
                        settings,
                        NULL);
}

static void
dispose(GObject *object)
{
    NMPolicy        *self = NM_POLICY(object);
    NMPolicyPrivate *priv = NM_POLICY_GET_PRIVATE(self);

    nm_assert(c_list_is_empty(&priv->policy_auto_activate_lst_head));
    nm_assert(g_hash_table_size(priv->devices) == 0);

    nm_clear_g_object(&priv->default_ac4);
    nm_clear_g_object(&priv->default_ac6);
    nm_clear_g_object(&priv->activating_ac4);
    nm_clear_g_object(&priv->activating_ac6);
    nm_clear_pointer(&priv->pending_active_connections, g_hash_table_unref);

    g_slist_free_full(priv->pending_secondaries, (GDestroyNotify) pending_secondary_data_free);
    priv->pending_secondaries = NULL;

    if (priv->firewalld_manager) {
        g_signal_handlers_disconnect_by_func(priv->firewalld_manager, firewall_state_changed, self);
        g_clear_object(&priv->firewalld_manager);
    }

    if (priv->agent_mgr) {
        g_signal_handlers_disconnect_by_func(priv->agent_mgr, secret_agent_registered, self);
        g_clear_object(&priv->agent_mgr);
    }

    if (priv->dns_manager) {
        nm_clear_g_signal_handler(priv->dns_manager, &priv->config_changed_id);
        g_clear_object(&priv->dns_manager);
    }

    /* The manager should have disposed of ActiveConnections already, which
     * will have called active_connection_removed() and thus we don't need
     * to clean anything up.  Assert that this is TRUE.
     */
    nm_assert(c_list_is_empty(nm_manager_get_active_connections(priv->manager)));

    nm_clear_g_source_inst(&priv->reset_connections_retries_idle_source);
    nm_clear_g_source_inst(&priv->device_recheck_auto_activate_all_idle_source);
    nm_clear_g_source_inst(&priv->hostname_retry.source);

    nm_clear_g_free(&priv->orig_hostname);
    nm_clear_g_free(&priv->cur_hostname);
    nm_clear_g_free(&priv->cur_hostname_full);
    nm_clear_g_free(&priv->last_hostname);

    if (priv->hostname_manager) {
        g_signal_handlers_disconnect_by_data(priv->hostname_manager, priv);
        g_clear_object(&priv->hostname_manager);
    }

    if (priv->settings) {
        g_signal_handlers_disconnect_by_data(priv->settings, priv);
        g_clear_object(&priv->settings);

        /* we don't clear priv->manager as we don't own a reference to it,
         * that is, NMManager must outlive NMPolicy anyway.
         *
         * Hence, we unsubscribe the signals here together with the signals
         * for settings. */
        g_signal_handlers_disconnect_by_data(priv->manager, priv);
    }

    if (priv->ip6_prefix_delegations) {
        g_array_free(priv->ip6_prefix_delegations, TRUE);
        priv->ip6_prefix_delegations = NULL;
    }

    nm_assert(NM_IS_MANAGER(priv->manager));

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

static void
finalize(GObject *object)
{
    NMPolicy        *self = NM_POLICY(object);
    NMPolicyPrivate *priv = NM_POLICY_GET_PRIVATE(self);

    g_hash_table_unref(priv->devices);

    G_OBJECT_CLASS(nm_policy_parent_class)->finalize(object);

    g_object_unref(priv->netns);
}

static void
nm_policy_class_init(NMPolicyClass *policy_class)
{
    GObjectClass *object_class = G_OBJECT_CLASS(policy_class);

    object_class->get_property = get_property;
    object_class->set_property = set_property;
    object_class->constructed  = constructed;
    object_class->dispose      = dispose;
    object_class->finalize     = finalize;

    obj_properties[PROP_MANAGER] =
        g_param_spec_object(NM_POLICY_MANAGER,
                            "",
                            "",
                            NM_TYPE_MANAGER,
                            G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
    obj_properties[PROP_SETTINGS] =
        g_param_spec_object(NM_POLICY_SETTINGS,
                            "",
                            "",
                            NM_TYPE_SETTINGS,
                            G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
    obj_properties[PROP_DEFAULT_IP4_AC] =
        g_param_spec_object(NM_POLICY_DEFAULT_IP4_AC,
                            "",
                            "",
                            NM_TYPE_ACTIVE_CONNECTION,
                            G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
    obj_properties[PROP_DEFAULT_IP6_AC] =
        g_param_spec_object(NM_POLICY_DEFAULT_IP6_AC,
                            "",
                            "",
                            NM_TYPE_DEVICE,
                            G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
    obj_properties[PROP_ACTIVATING_IP4_AC] =
        g_param_spec_object(NM_POLICY_ACTIVATING_IP4_AC,
                            "",
                            "",
                            NM_TYPE_DEVICE,
                            G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
    obj_properties[PROP_ACTIVATING_IP6_AC] =
        g_param_spec_object(NM_POLICY_ACTIVATING_IP6_AC,
                            "",
                            "",
                            NM_TYPE_DEVICE,
                            G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);

    g_object_class_install_properties(object_class, _PROPERTY_ENUMS_LAST, obj_properties);
}
