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

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

#include "nm-supplicant-interface.h"

#include <stdio.h>
#include <linux/if_ether.h>

#include "NetworkManagerUtils.h"
#include "libnm-core-intern/nm-core-internal.h"
#include "libnm-glib-aux/nm-c-list.h"
#include "libnm-glib-aux/nm-ref-string.h"
#include "libnm-glib-aux/nm-dbus-aux.h"
#include "libnm-std-aux/nm-dbus-compat.h"
#include "nm-supplicant-config.h"
#include "nm-supplicant-manager.h"

#define DBUS_TIMEOUT_MSEC 20000

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

typedef struct {
    NMSupplicantInterface *self;
    char *                 type;
    char *                 bssid;
    char *                 pin;
    guint                  signal_id;
    GCancellable *         cancellable;
    bool                   needs_cancelling : 1;
    bool                   is_cancelling : 1;
} WpsData;

struct _AddNetworkData;

typedef struct {
    NMSupplicantInterface *      self;
    NMSupplicantConfig *         cfg;
    GCancellable *               cancellable;
    NMSupplicantInterfaceAssocCb callback;
    gpointer                     user_data;
    guint                        fail_on_idle_id;
    guint                        blobs_left;
    guint                        calls_left;
    struct _AddNetworkData *     add_network_data;
} AssocData;

typedef struct _AddNetworkData {
    /* the assoc_data at the time when doing the call. */
    AssocData *  assoc_data;
    NMRefString *name_owner;
    NMRefString *object_path;
    GObject *    shutdown_wait_obj;
} AddNetworkData;

enum {
    STATE,           /* change in the interface's state */
    BSS_CHANGED,     /* a new BSS appeared, was updated, or was removed. */
    PEER_CHANGED,    /* a new Peer appeared, was updated, or was removed */
    WPS_CREDENTIALS, /* WPS credentials received */
    GROUP_STARTED,   /* a new Group (interface) was created */
    GROUP_FINISHED,  /* a Group (interface) has been finished */
    LAST_SIGNAL
};

static guint signals[LAST_SIGNAL] = {0};

NM_GOBJECT_PROPERTIES_DEFINE(NMSupplicantInterface,
                             PROP_SUPPLICANT_MANAGER,
                             PROP_DBUS_OBJECT_PATH,
                             PROP_IFINDEX,
                             PROP_P2P_GROUP_JOINED,
                             PROP_P2P_GROUP_PATH,
                             PROP_P2P_GROUP_OWNER,
                             PROP_SCANNING,
                             PROP_CURRENT_BSS,
                             PROP_DRIVER,
                             PROP_P2P_AVAILABLE,
                             PROP_AUTH_STATE, );

typedef struct _NMSupplicantInterfacePrivate {
    NMSupplicantManager *supplicant_manager;

    GDBusConnection *dbus_connection;
    NMRefString *    name_owner;
    NMRefString *    object_path;

    char *ifname;

    GCancellable *main_cancellable;

    NMRefString *p2p_group_path;

    GCancellable *p2p_group_properties_cancellable;

    WpsData *wps_data;

    AssocData *assoc_data;

    char *net_path;

    char *driver;

    GHashTable *bss_idx;
    CList       bss_lst_head;
    CList       bss_initializing_lst_head;

    NMRefString *current_bss;

    GHashTable *peer_idx;
    CList       peer_lst_head;
    CList       peer_initializing_lst_head;

    gint64 last_scan_msec;

    NMSupplicantAuthState auth_state;

    NMSupplicantDriver requested_driver;
    NMSupplCapMask     global_capabilities;
    NMSupplCapMask     iface_capabilities;

    guint properties_changed_id;
    guint signal_id;
    guint bss_properties_changed_id;
    guint peer_properties_changed_id;
    guint p2p_group_properties_changed_id;

    int ifindex;

    int starting_pending_count;

    guint32 max_scan_ssids;

    gint32 disconnect_reason;

    NMSupplicantInterfaceState state;
    NMSupplicantInterfaceState supp_state;

    bool scanning_property : 1;
    bool scanning_cached : 1;

    bool p2p_capable_property : 1;
    bool p2p_capable_cached : 1;

    bool p2p_group_owner_property : 1;
    bool p2p_group_owner_cached : 1;

    bool p2p_group_joined_cached : 1;

    bool is_ready_main : 1;
    bool is_ready_p2p_device : 1;

    bool prop_scan_active : 1;
    bool prop_scan_ssid : 1;

    bool ap_isolate_supported : 1;
    bool ap_isolate_needs_reset : 1;
} NMSupplicantInterfacePrivate;

struct _NMSupplicantInterfaceClass {
    GObjectClass parent;
};

G_DEFINE_TYPE(NMSupplicantInterface, nm_supplicant_interface, G_TYPE_OBJECT)

#define NM_SUPPLICANT_INTERFACE_GET_PRIVATE(self) \
    _NM_GET_PRIVATE_PTR(self, NMSupplicantInterface, NM_IS_SUPPLICANT_INTERFACE)

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

static const char *
_log_pretty_object_path(NMSupplicantInterfacePrivate *priv)
{
    const char *s;

    nm_assert(priv);
    nm_assert(NM_IS_REF_STRING(priv->object_path));

    s = priv->object_path->str;
    if (NM_STR_HAS_PREFIX(s, "/fi/w1/wpa_supplicant1/Interfaces/")) {
        s += NM_STRLEN("/fi/w1/wpa_supplicant1/Interfaces/");
        if (s[0] && s[0] != '/')
            return s;
    }
    return priv->object_path->str;
}

#define _NMLOG_DOMAIN      LOGD_SUPPLICANT
#define _NMLOG_PREFIX_NAME "sup-iface"
#define _NMLOG(level, ...)                                                      \
    G_STMT_START                                                                \
    {                                                                           \
        NMSupplicantInterface *       _self = (self);                           \
        NMSupplicantInterfacePrivate *_priv =                                   \
            _self ? NM_SUPPLICANT_INTERFACE_GET_PRIVATE(_self) : NULL;          \
        char        _sbuf[255];                                                 \
        const char *_ifname = _priv ? _priv->ifname : NULL;                     \
                                                                                \
        nm_log((level),                                                         \
               _NMLOG_DOMAIN,                                                   \
               _ifname,                                                         \
               NULL,                                                            \
               "%s%s: " _NM_UTILS_MACRO_FIRST(__VA_ARGS__),                     \
               _NMLOG_PREFIX_NAME,                                              \
               (_self ? nm_sprintf_buf(_sbuf,                                   \
                                       "[" NM_HASH_OBFUSCATE_PTR_FMT ",%s,%s]", \
                                       NM_HASH_OBFUSCATE_PTR(_self),            \
                                       _log_pretty_object_path(_priv),          \
                                       _ifname ?: "???")                        \
                      : "") _NM_UTILS_MACRO_REST(__VA_ARGS__));                 \
    }                                                                           \
    G_STMT_END

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

static void _starting_check_ready(NMSupplicantInterface *self);

static void assoc_return(NMSupplicantInterface *self, GError *error, const char *message);

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

NM_UTILS_LOOKUP_STR_DEFINE(
    nm_supplicant_interface_state_to_string,
    NMSupplicantInterfaceState,
    NM_UTILS_LOOKUP_DEFAULT_WARN("internal-unknown"),
    NM_UTILS_LOOKUP_STR_ITEM(NM_SUPPLICANT_INTERFACE_STATE_INVALID, "internal-invalid"),
    NM_UTILS_LOOKUP_STR_ITEM(NM_SUPPLICANT_INTERFACE_STATE_STARTING, "internal-starting"),

    NM_UTILS_LOOKUP_STR_ITEM(NM_SUPPLICANT_INTERFACE_STATE_4WAY_HANDSHAKE, "4way_handshake"),
    NM_UTILS_LOOKUP_STR_ITEM(NM_SUPPLICANT_INTERFACE_STATE_ASSOCIATED, "associated"),
    NM_UTILS_LOOKUP_STR_ITEM(NM_SUPPLICANT_INTERFACE_STATE_ASSOCIATING, "associating"),
    NM_UTILS_LOOKUP_STR_ITEM(NM_SUPPLICANT_INTERFACE_STATE_AUTHENTICATING, "authenticating"),
    NM_UTILS_LOOKUP_STR_ITEM(NM_SUPPLICANT_INTERFACE_STATE_COMPLETED, "completed"),
    NM_UTILS_LOOKUP_STR_ITEM(NM_SUPPLICANT_INTERFACE_STATE_DISCONNECTED, "disconnected"),
    NM_UTILS_LOOKUP_STR_ITEM(NM_SUPPLICANT_INTERFACE_STATE_GROUP_HANDSHAKE, "group_handshake"),
    NM_UTILS_LOOKUP_STR_ITEM(NM_SUPPLICANT_INTERFACE_STATE_INACTIVE, "inactive"),
    NM_UTILS_LOOKUP_STR_ITEM(NM_SUPPLICANT_INTERFACE_STATE_DISABLED, "interface_disabled"),
    NM_UTILS_LOOKUP_STR_ITEM(NM_SUPPLICANT_INTERFACE_STATE_SCANNING, "scanning"),

    NM_UTILS_LOOKUP_STR_ITEM(NM_SUPPLICANT_INTERFACE_STATE_DOWN, "internal-down"), );

static NM_UTILS_STRING_TABLE_LOOKUP_DEFINE(
    wpas_state_string_to_enum,
    NMSupplicantInterfaceState,
    { nm_assert(name); },
    { return NM_SUPPLICANT_INTERFACE_STATE_INVALID; },
    {"4way_handshake", NM_SUPPLICANT_INTERFACE_STATE_4WAY_HANDSHAKE},
    {"associated", NM_SUPPLICANT_INTERFACE_STATE_ASSOCIATED},
    {"associating", NM_SUPPLICANT_INTERFACE_STATE_ASSOCIATING},
    {"authenticating", NM_SUPPLICANT_INTERFACE_STATE_AUTHENTICATING},
    {"completed", NM_SUPPLICANT_INTERFACE_STATE_COMPLETED},
    {"disconnected", NM_SUPPLICANT_INTERFACE_STATE_DISCONNECTED},
    {"group_handshake", NM_SUPPLICANT_INTERFACE_STATE_GROUP_HANDSHAKE},
    {"inactive", NM_SUPPLICANT_INTERFACE_STATE_INACTIVE},
    {"interface_disabled", NM_SUPPLICANT_INTERFACE_STATE_DISABLED},
    {"scanning", NM_SUPPLICANT_INTERFACE_STATE_SCANNING}, );

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

static NM80211ApSecurityFlags
security_from_vardict(GVariant *security)
{
    NM80211ApSecurityFlags flags = NM_802_11_AP_SEC_NONE;
    const char **          array;
    const char *           tmp;
    gsize                  i;
    const char *           v;

    nm_assert(g_variant_is_of_type(security, G_VARIANT_TYPE_VARDICT));

    if (g_variant_lookup(security, "KeyMgmt", "^a&s", &array)) {
        for (i = 0; (v = array[i]); i++) {
            if (NM_IN_STRSET(v, "wpa-psk", "wpa-psk-sha256", "wpa-ft-psk"))
                flags |= NM_802_11_AP_SEC_KEY_MGMT_PSK;
            else if (NM_IN_STRSET(v,
                                  "wpa-eap",
                                  "wpa-eap-sha256",
                                  "wpa-ft-eap",
                                  "wpa-fils-sha256",
                                  "wpa-fils-sha384",
                                  "wpa-fils-ft-sha256",
                                  "wpa-fils-ft-sha384"))
                flags |= NM_802_11_AP_SEC_KEY_MGMT_802_1X;
            else if (NM_IN_STRSET(v, "sae", "ft-sae"))
                flags |= NM_802_11_AP_SEC_KEY_MGMT_SAE;
            else if (NM_IN_STRSET(v, "owe"))
                flags |= NM_802_11_AP_SEC_KEY_MGMT_OWE;
            else if (NM_IN_STRSET(v, "wpa-eap-suite-b-192", "wpa-ft-eap-sha384"))
                flags |= NM_802_11_AP_SEC_KEY_MGMT_EAP_SUITE_B_192;
        }
        g_free(array);
    }

    if (g_variant_lookup(security, "Pairwise", "^a&s", &array)) {
        for (i = 0; (v = array[i]); i++) {
            if (NM_IN_STRSET(v, "tkip"))
                flags |= NM_802_11_AP_SEC_PAIR_TKIP;
            else if (NM_IN_STRSET(v, "ccmp"))
                flags |= NM_802_11_AP_SEC_PAIR_CCMP;
        }
        g_free(array);
    }

    if (g_variant_lookup(security, "Group", "&s", &tmp)) {
        if (nm_streq(tmp, "wep40"))
            flags |= NM_802_11_AP_SEC_GROUP_WEP40;
        else if (nm_streq(tmp, "wep104"))
            flags |= NM_802_11_AP_SEC_GROUP_WEP104;
        else if (nm_streq(tmp, "tkip"))
            flags |= NM_802_11_AP_SEC_GROUP_TKIP;
        else if (nm_streq(tmp, "ccmp"))
            flags |= NM_802_11_AP_SEC_GROUP_CCMP;
    }

    return flags;
}

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

/* Various conditions prevent _starting_check_ready() from completing. For example,
 * bss_initializing_lst_head, peer_initializing_lst_head and p2p_group_properties_cancellable.
 * At some places, these conditions might toggle, and it would seems we would have
 * to call _starting_check_ready() at that point, to ensure we don't miss a state
 * change that we are ready. However, these places are deep in the call stack and
 * not suitable to perform this state change. Instead, the callers *MUST* have
 * added their own starting_pending_count to delay _starting_check_ready().
 *
 * Assert that is the case. */
#define nm_assert_starting_has_pending_count(v) nm_assert((v) > 0)

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

static void
_dbus_connection_call(NMSupplicantInterface *self,
                      const char *           interface_name,
                      const char *           method_name,
                      GVariant *             parameters,
                      const GVariantType *   reply_type,
                      GDBusCallFlags         flags,
                      int                    timeout_msec,
                      GCancellable *         cancellable,
                      GAsyncReadyCallback    callback,
                      gpointer               user_data)
{
    NMSupplicantInterfacePrivate *priv = NM_SUPPLICANT_INTERFACE_GET_PRIVATE(self);

    g_dbus_connection_call(priv->dbus_connection,
                           priv->name_owner->str,
                           priv->object_path->str,
                           interface_name,
                           method_name,
                           parameters,
                           reply_type,
                           flags,
                           timeout_msec,
                           cancellable,
                           callback,
                           user_data);
}

static void
_dbus_connection_call_simple_cb(GObject *source, GAsyncResult *result, gpointer user_data)
{
    NMSupplicantInterface *self;
    gs_unref_variant GVariant *res = NULL;
    gs_free_error GError *error    = NULL;
    const char *          log_reason;
    gs_free char *        remote_error = NULL;
    gpointer              p_suppress_warning;
    gboolean              suppress_warning;

    nm_utils_user_data_unpack(user_data, &self, &log_reason, &p_suppress_warning);

    suppress_warning = GPOINTER_TO_INT(p_suppress_warning);

    res = g_dbus_connection_call_finish(G_DBUS_CONNECTION(source), result, &error);
    if (nm_utils_error_is_cancelled(error))
        return;

    if (res) {
        _LOGT("call-%s: success", log_reason);
        return;
    }

    if (!suppress_warning) {
        remote_error = g_dbus_error_get_remote_error(error);
        if (!nm_streq0(remote_error, "fi.w1.wpa_supplicant1.NotConnected")) {
            g_dbus_error_strip_remote_error(error);
            _LOGW("call-%s: failed with %s", log_reason, error->message);
            return;
        }
    }

    _LOGT("call-%s: failed with %s", log_reason, error->message);
}

static void
_dbus_connection_call_simple_full_impl(NMSupplicantInterface *self,
                                       const char *           interface_name,
                                       const char *           method_name,
                                       GVariant *             parameters,
                                       const GVariantType *   reply_type,
                                       const char *           log_reason,
                                       gboolean               suppress_warning)
{
    NMSupplicantInterfacePrivate *priv = NM_SUPPLICANT_INTERFACE_GET_PRIVATE(self);

    _dbus_connection_call(
        self,
        interface_name,
        method_name,
        parameters,
        reply_type,
        G_DBUS_CALL_FLAGS_NONE,
        DBUS_TIMEOUT_MSEC,
        priv->main_cancellable,
        _dbus_connection_call_simple_cb,
        nm_utils_user_data_pack(self, log_reason, GINT_TO_POINTER(suppress_warning)));
}

#define _dbus_connection_call_simple_full(self,              \
                                          interface_name,    \
                                          method_name,       \
                                          parameters,        \
                                          reply_type,        \
                                          log_reason,        \
                                          suppress_warning)  \
    _dbus_connection_call_simple_full_impl((self),           \
                                           (interface_name), \
                                           (method_name),    \
                                           (parameters),     \
                                           (reply_type),     \
                                           "" log_reason "", \
                                           (suppress_warning))

#define _dbus_connection_call_simple(self,                   \
                                     interface_name,         \
                                     method_name,            \
                                     parameters,             \
                                     reply_type,             \
                                     log_reason)             \
    _dbus_connection_call_simple_full_impl((self),           \
                                           (interface_name), \
                                           (method_name),    \
                                           (parameters),     \
                                           (reply_type),     \
                                           "" log_reason "", \
                                           FALSE)

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

static void
_emit_signal_state(NMSupplicantInterface *    self,
                   NMSupplicantInterfaceState new_state,
                   NMSupplicantInterfaceState old_state,
                   gint32                     disconnect_reason)
{
    g_signal_emit(self,
                  signals[STATE],
                  0,
                  (int) new_state,
                  (int) old_state,
                  (int) disconnect_reason);
}

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

static void
_remove_network(NMSupplicantInterface *self)
{
    NMSupplicantInterfacePrivate *priv     = NM_SUPPLICANT_INTERFACE_GET_PRIVATE(self);
    gs_free char *                net_path = NULL;

    if (!priv->net_path)
        return;

    net_path = g_steal_pointer(&priv->net_path);
    _dbus_connection_call_simple(self,
                                 NM_WPAS_DBUS_IFACE_INTERFACE,
                                 "RemoveNetwork",
                                 g_variant_new("(o)", net_path),
                                 G_VARIANT_TYPE("()"),
                                 "remove-network");

    if (priv->ap_isolate_supported && priv->ap_isolate_needs_reset) {
        _dbus_connection_call_simple(self,
                                     DBUS_INTERFACE_PROPERTIES,
                                     "Set",
                                     g_variant_new("(ssv)",
                                                   NM_WPAS_DBUS_IFACE_INTERFACE,
                                                   "ApIsolate",
                                                   g_variant_new_string("0")),
                                     G_VARIANT_TYPE("()"),
                                     "reset-ap-isolation");
    }
    priv->ap_isolate_needs_reset = FALSE;
}

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

static void
_notify_maybe_scanning(NMSupplicantInterface *self)
{
    NMSupplicantInterfacePrivate *priv = NM_SUPPLICANT_INTERFACE_GET_PRIVATE(self);
    gboolean                      scanning;

    scanning = nm_supplicant_interface_state_is_operational(priv->state) && priv->scanning_property;

    if (priv->scanning_cached == scanning)
        return;

    if (!scanning && !c_list_is_empty(&priv->bss_initializing_lst_head)) {
        /* we would change state to indicate we no longer scan. However,
         * we still have BSS instances to be initialized. Delay the
         * state change further. */
        return;
    }

    _LOGT("scanning: %s", scanning ? "yes" : "no");

    if (!scanning)
        priv->last_scan_msec = nm_utils_get_monotonic_timestamp_msec();
    else {
        /* while we are scanning, we set the timestamp to -1. */
        priv->last_scan_msec = -1;
    }
    priv->scanning_cached = scanning;
    _notify(self, PROP_SCANNING);
}

static void
_notify_maybe_p2p_available(NMSupplicantInterface *self)
{
    NMSupplicantInterfacePrivate *priv = NM_SUPPLICANT_INTERFACE_GET_PRIVATE(self);
    gboolean                      value;

    value = priv->is_ready_p2p_device && priv->p2p_capable_property;

    if (priv->p2p_capable_cached == value)
        return;

    priv->p2p_capable_cached = value;
    _notify(self, PROP_P2P_AVAILABLE);
}

static void
_notify_maybe_p2p_group(NMSupplicantInterface *self)
{
    NMSupplicantInterfacePrivate *priv = NM_SUPPLICANT_INTERFACE_GET_PRIVATE(self);
    gboolean                      value_joined;
    gboolean                      value_owner;
    gboolean                      joined_changed;
    gboolean                      owner_changed;

    value_joined = priv->p2p_group_path && !priv->p2p_group_properties_cancellable;
    value_owner  = value_joined && priv->p2p_group_owner_property;

    if ((joined_changed = (priv->p2p_group_joined_cached != value_joined)))
        priv->p2p_group_joined_cached = value_joined;

    if ((owner_changed = (priv->p2p_group_owner_cached != value_owner)))
        priv->p2p_group_owner_cached = value_owner;

    if (joined_changed)
        _notify(self, PROP_P2P_GROUP_JOINED);
    if (owner_changed)
        _notify(self, PROP_P2P_GROUP_OWNER);
}

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

static void
_bss_info_destroy(NMSupplicantBssInfo *bss_info)
{
    c_list_unlink_stale(&bss_info->_bss_lst);
    nm_clear_g_cancellable(&bss_info->_init_cancellable);
    g_bytes_unref(bss_info->ssid);
    nm_ref_string_unref(bss_info->bss_path);
    nm_g_slice_free(bss_info);
}

static void
_bss_info_changed_emit(NMSupplicantInterface *self,
                       NMSupplicantBssInfo *  bss_info,
                       gboolean               is_present)
{
    _LOGT("BSS %s %s", bss_info->bss_path->str, is_present ? "updated" : "deleted");
    g_signal_emit(self, signals[BSS_CHANGED], 0, bss_info, is_present);
}

static void
_bss_info_properties_changed(NMSupplicantInterface *self,
                             NMSupplicantBssInfo *  bss_info,
                             GVariant *             properties,
                             gboolean               initial)
{
    gboolean       v_b;
    GVariant *     v_v;
    const char *   v_s;
    gint16         v_i16;
    guint16        v_u16;
    guint32        v_u32;
    NM80211ApFlags p_ap_flags;
    _NM80211Mode   p_mode;
    guint8         p_signal_percent;
    const guint8 * arr_data;
    gsize          arr_len;
    guint32        p_max_rate;
    gboolean       p_max_rate_has;
    gint64         now_msec = 0;

    if (nm_g_variant_lookup(properties, "Age", "u", &v_u32)) {
        bss_info->last_seen_msec =
            nm_utils_get_monotonic_timestamp_msec_cached(&now_msec) - (((gint64) v_u32) * 1000);
    } else if (initial) {
        /* Unknown Age. Assume we just received it. */
        bss_info->last_seen_msec = nm_utils_get_monotonic_timestamp_msec_cached(&now_msec);
    }

    p_ap_flags = bss_info->ap_flags;
    if (nm_g_variant_lookup(properties, "Privacy", "b", &v_b))
        p_ap_flags = NM_FLAGS_ASSIGN(p_ap_flags, NM_802_11_AP_FLAGS_PRIVACY, v_b);
    else {
        nm_assert(!initial || !NM_FLAGS_HAS(p_ap_flags, NM_802_11_AP_FLAGS_PRIVACY));
    }
    v_v = nm_g_variant_lookup_value(properties, "WPS", G_VARIANT_TYPE_VARDICT);
    if (v_v || initial) {
        NM80211ApFlags f = NM_802_11_AP_FLAGS_NONE;

        if (v_v) {
            if (g_variant_lookup(v_v, "Type", "&s", &v_s)) {
                f = NM_802_11_AP_FLAGS_WPS;
                if (nm_streq(v_s, "pbc"))
                    f |= NM_802_11_AP_FLAGS_WPS_PBC;
                else if (nm_streq(v_s, "pin"))
                    f |= NM_802_11_AP_FLAGS_WPS_PIN;
            }
            g_variant_unref(v_v);
        }
        p_ap_flags = NM_FLAGS_ASSIGN_MASK(p_ap_flags,
                                          NM_802_11_AP_FLAGS_WPS | NM_802_11_AP_FLAGS_WPS_PBC
                                              | NM_802_11_AP_FLAGS_WPS_PIN,
                                          f);
    }
    if (bss_info->ap_flags != p_ap_flags) {
        bss_info->ap_flags = p_ap_flags;
        nm_assert(bss_info->ap_flags == p_ap_flags);
    }

    if (nm_g_variant_lookup(properties, "Mode", "&s", &v_s)) {
        if (nm_streq(v_s, "infrastructure"))
            p_mode = _NM_802_11_MODE_INFRA;
        else if (nm_streq(v_s, "ad-hoc"))
            p_mode = _NM_802_11_MODE_ADHOC;
        else if (nm_streq(v_s, "mesh"))
            p_mode = _NM_802_11_MODE_MESH;
        else
            p_mode = _NM_802_11_MODE_UNKNOWN;
    } else if (initial)
        p_mode = _NM_802_11_MODE_UNKNOWN;
    else
        p_mode = bss_info->mode;
    if (bss_info->mode != p_mode) {
        bss_info->mode = p_mode;
        nm_assert(bss_info->mode == p_mode);
    }

    if (nm_g_variant_lookup(properties, "Signal", "n", &v_i16))
        p_signal_percent = nm_wifi_utils_level_to_quality(v_i16);
    else if (initial)
        p_signal_percent = 0;
    else
        p_signal_percent = bss_info->signal_percent;
    bss_info->signal_percent = p_signal_percent;

    if (nm_g_variant_lookup(properties, "Frequency", "q", &v_u16))
        bss_info->frequency = v_u16;

    v_v = nm_g_variant_lookup_value(properties, "SSID", G_VARIANT_TYPE_BYTESTRING);
    if (v_v) {
        arr_data = g_variant_get_fixed_array(v_v, &arr_len, 1);
        arr_len  = MIN(32, arr_len);

        /* Stupid ieee80211 layer uses <hidden> */
        if (arr_data && arr_len
            && !(NM_IN_SET(arr_len, 8, 9) && memcmp(arr_data, "<hidden>", arr_len) == 0)
            && !nm_utils_is_empty_ssid(arr_data, arr_len)) {
            /* good */
        } else
            arr_len = 0;

        if (!nm_utils_gbytes_equal_mem(bss_info->ssid, arr_data, arr_len)) {
            _nm_unused gs_unref_bytes GBytes *old_free = g_steal_pointer(&bss_info->ssid);

            bss_info->ssid = (arr_len == 0) ? NULL : g_bytes_new(arr_data, arr_len);
        }

        g_variant_unref(v_v);
    } else {
        nm_assert(!initial || !bss_info->ssid);
    }

    v_v = nm_g_variant_lookup_value(properties, "BSSID", G_VARIANT_TYPE_BYTESTRING);
    if (v_v) {
        arr_data = g_variant_get_fixed_array(v_v, &arr_len, 1);
        if (arr_len == ETH_ALEN && memcmp(arr_data, &nm_ether_addr_zero, ETH_ALEN) != 0
            && memcmp(arr_data, (char[ETH_ALEN]){0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}, ETH_ALEN)
                   != 0) {
            /* pass */
        } else
            arr_len = 0;

        if (arr_len != 0) {
            nm_assert(arr_len == sizeof(bss_info->bssid));
            bss_info->bssid_valid = TRUE;
            memcpy(&bss_info->bssid, arr_data, sizeof(bss_info->bssid));
        } else if (bss_info->bssid_valid) {
            bss_info->bssid_valid = FALSE;
            memset(&bss_info->bssid, 0, sizeof(bss_info->bssid));
        }
        g_variant_unref(v_v);
    } else {
        nm_assert(!initial || !bss_info->bssid_valid);
    }
    nm_assert((!!bss_info->bssid_valid)
              == (!nm_utils_memeqzero(&bss_info->bssid, sizeof(bss_info->bssid))));

    p_max_rate_has = FALSE;
    p_max_rate     = 0;
    v_v            = nm_g_variant_lookup_value(properties, "Rates", G_VARIANT_TYPE("au"));
    if (v_v) {
        const guint32 *rates = g_variant_get_fixed_array(v_v, &arr_len, sizeof(guint32));
        gsize          i;

        for (i = 0; i < arr_len; i++)
            p_max_rate = NM_MAX(p_max_rate, rates[i]);
        p_max_rate_has = TRUE;
        g_variant_unref(v_v);
    }

    v_v = nm_g_variant_lookup_value(properties, "WPA", G_VARIANT_TYPE_VARDICT);
    if (v_v) {
        bss_info->wpa_flags = security_from_vardict(v_v);
        g_variant_unref(v_v);
    }

    v_v = nm_g_variant_lookup_value(properties, "RSN", G_VARIANT_TYPE_VARDICT);
    if (v_v) {
        bss_info->rsn_flags = security_from_vardict(v_v);
        g_variant_unref(v_v);
    }

    v_v = nm_g_variant_lookup_value(properties, "IEs", G_VARIANT_TYPE_BYTESTRING);
    if (v_v) {
        gboolean p_owe_transition_mode;
        gboolean p_metered;
        guint32  rate;

        arr_data = g_variant_get_fixed_array(v_v, &arr_len, 1);
        nm_wifi_utils_parse_ies(arr_data, arr_len, &rate, &p_metered, &p_owe_transition_mode);
        p_max_rate     = NM_MAX(p_max_rate, rate);
        p_max_rate_has = TRUE;
        g_variant_unref(v_v);

        if (p_owe_transition_mode)
            bss_info->rsn_flags |= NM_802_11_AP_SEC_KEY_MGMT_OWE_TM;
        else
            bss_info->rsn_flags &= ~NM_802_11_AP_SEC_KEY_MGMT_OWE_TM;

        bss_info->metered = p_metered;
    }

    if (p_max_rate_has)
        bss_info->max_rate = p_max_rate / 1000u;

    _bss_info_changed_emit(self, bss_info, TRUE);
}

static void
_bss_info_get_all_cb(GVariant *result, GError *error, gpointer user_data)
{
    NMSupplicantBssInfo *         bss_info;
    NMSupplicantInterface *       self;
    NMSupplicantInterfacePrivate *priv;
    gs_unref_variant GVariant *properties = NULL;

    if (nm_utils_error_is_cancelled(error))
        return;

    bss_info = user_data;
    self     = bss_info->_self;
    priv     = NM_SUPPLICANT_INTERFACE_GET_PRIVATE(self);

    g_clear_object(&bss_info->_init_cancellable);
    nm_c_list_move_tail(&priv->bss_lst_head, &bss_info->_bss_lst);

    if (result)
        g_variant_get(result, "(@a{sv})", &properties);

    _bss_info_properties_changed(self, bss_info, properties, TRUE);

    _starting_check_ready(self);

    _notify_maybe_scanning(self);
}

static void
_bss_info_add(NMSupplicantInterface *self, const char *object_path)
{
    NMSupplicantInterfacePrivate *priv       = NM_SUPPLICANT_INTERFACE_GET_PRIVATE(self);
    nm_auto_ref_string NMRefString *bss_path = NULL;
    NMSupplicantBssInfo *           bss_info;

    bss_path = nm_ref_string_new(nm_dbus_path_not_empty(object_path));
    if (!bss_path)
        return;

    bss_info = g_hash_table_lookup(priv->bss_idx, &bss_path);
    if (bss_info) {
        bss_info->_bss_dirty = FALSE;
        return;
    }

    bss_info  = g_slice_new(NMSupplicantBssInfo);
    *bss_info = (NMSupplicantBssInfo){
        ._self             = self,
        .bss_path          = g_steal_pointer(&bss_path),
        ._init_cancellable = g_cancellable_new(),
    };
    c_list_link_tail(&priv->bss_initializing_lst_head, &bss_info->_bss_lst);
    g_hash_table_add(priv->bss_idx, bss_info);

    nm_dbus_connection_call_get_all(priv->dbus_connection,
                                    priv->name_owner->str,
                                    bss_info->bss_path->str,
                                    NM_WPAS_DBUS_IFACE_BSS,
                                    5000,
                                    bss_info->_init_cancellable,
                                    _bss_info_get_all_cb,
                                    bss_info);
}

static gboolean
_bss_info_remove(NMSupplicantInterface *self, NMRefString **p_bss_path)
{
    NMSupplicantInterfacePrivate *priv = NM_SUPPLICANT_INTERFACE_GET_PRIVATE(self);
    NMSupplicantBssInfo *         bss_info;
    gpointer                      unused_but_required;

    if (!g_hash_table_steal_extended(priv->bss_idx,
                                     p_bss_path,
                                     (gpointer *) &bss_info,
                                     &unused_but_required))
        return FALSE;

    c_list_unlink(&bss_info->_bss_lst);
    if (!bss_info->_init_cancellable)
        _bss_info_changed_emit(self, bss_info, FALSE);
    _bss_info_destroy(bss_info);

    nm_assert_starting_has_pending_count(priv->starting_pending_count);

    return TRUE;
}

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

static void
_peer_info_destroy(NMSupplicantPeerInfo *peer_info)
{
    c_list_unlink(&peer_info->_peer_lst);
    nm_clear_g_cancellable(&peer_info->_init_cancellable);

    g_free(peer_info->device_name);
    g_free(peer_info->manufacturer);
    g_free(peer_info->model);
    g_free(peer_info->model_number);
    g_free(peer_info->serial);
    g_free(peer_info->groups);
    g_bytes_unref(peer_info->ies);

    nm_ref_string_unref(peer_info->peer_path);

    nm_g_slice_free(peer_info);
}

static void
_peer_info_changed_emit(NMSupplicantInterface *self,
                        NMSupplicantPeerInfo * peer_info,
                        gboolean               is_present)
{
    g_signal_emit(self, signals[PEER_CHANGED], 0, peer_info, is_present);
}

static void
_peer_info_properties_changed(NMSupplicantInterface *self,
                              NMSupplicantPeerInfo * peer_info,
                              GVariant *             properties,
                              gboolean               initial)
{
    GVariant *    v_v;
    const char *  v_s;
    const char ** v_strv;
    gint32        v_i32;
    const guint8 *arr_data;
    gsize         arr_len;

    peer_info->last_seen_msec = nm_utils_get_monotonic_timestamp_msec();

    if (nm_g_variant_lookup(properties, "level", "i", &v_i32))
        peer_info->signal_percent = nm_wifi_utils_level_to_quality(v_i32);

    if (nm_g_variant_lookup(properties, "DeviceName", "&s", &v_s))
        nm_utils_strdup_reset(&peer_info->device_name, v_s);

    if (nm_g_variant_lookup(properties, "Manufacturer", "&s", &v_s))
        nm_utils_strdup_reset(&peer_info->manufacturer, v_s);

    if (nm_g_variant_lookup(properties, "Model", "&s", &v_s))
        nm_utils_strdup_reset(&peer_info->model, v_s);

    if (nm_g_variant_lookup(properties, "ModelNumber", "&s", &v_s))
        nm_utils_strdup_reset(&peer_info->model_number, v_s);

    if (nm_g_variant_lookup(properties, "Serial", "&s", &v_s))
        nm_utils_strdup_reset(&peer_info->serial, v_s);

    if (nm_g_variant_lookup(properties, "Groups", "^a&o", &v_strv)) {
        g_free(peer_info->groups);
        peer_info->groups = nm_utils_strv_dup_packed(v_strv, -1);

        g_free(v_strv);
    }

    v_v = nm_g_variant_lookup_value(properties, "DeviceAddress", G_VARIANT_TYPE_BYTESTRING);
    if (v_v) {
        arr_data = g_variant_get_fixed_array(v_v, &arr_len, 1);
        if (arr_len == ETH_ALEN && memcmp(arr_data, &nm_ether_addr_zero, ETH_ALEN) != 0
            && memcmp(arr_data, (char[ETH_ALEN]){0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}, ETH_ALEN)
                   != 0) {
            /* pass */
        } else
            arr_len = 0;

        if (arr_len != 0) {
            nm_assert(arr_len == sizeof(peer_info->address));
            peer_info->address_valid = TRUE;
            memcpy(peer_info->address, arr_data, sizeof(peer_info->address));
        } else if (peer_info->address_valid) {
            peer_info->address_valid = FALSE;
            memset(peer_info->address, 0, sizeof(peer_info->address));
        }
        g_variant_unref(v_v);
    } else {
        nm_assert(!initial || !peer_info->address_valid);
    }
    nm_assert((peer_info->address_valid
               && !nm_utils_memeqzero(peer_info->address, sizeof(peer_info->address)))
              || (!peer_info->address_valid
                  && nm_utils_memeqzero(peer_info->address, sizeof(peer_info->address))));

    /* The IEs property contains the WFD R1 subelements */
    v_v = nm_g_variant_lookup_value(properties, "IEs", G_VARIANT_TYPE_BYTESTRING);
    if (v_v) {
        arr_data = g_variant_get_fixed_array(v_v, &arr_len, 1);
        if (!nm_utils_gbytes_equal_mem(peer_info->ies, arr_data, arr_len)) {
            _nm_unused gs_unref_bytes GBytes *old_free = g_steal_pointer(&peer_info->ies);

            peer_info->ies = g_bytes_new(arr_data, arr_len);
        } else if (arr_len == 0 && !peer_info->ies)
            peer_info->ies = g_bytes_new(NULL, 0);
        g_variant_unref(v_v);
    }

    _peer_info_changed_emit(self, peer_info, TRUE);
}

static void
_peer_info_get_all_cb(GVariant *result, GError *error, gpointer user_data)
{
    NMSupplicantPeerInfo *        peer_info;
    NMSupplicantInterface *       self;
    NMSupplicantInterfacePrivate *priv;
    gs_unref_variant GVariant *properties = NULL;

    if (nm_utils_error_is_cancelled(error))
        return;

    peer_info = user_data;
    self      = peer_info->_self;
    priv      = NM_SUPPLICANT_INTERFACE_GET_PRIVATE(self);

    g_clear_object(&peer_info->_init_cancellable);
    nm_c_list_move_tail(&priv->peer_lst_head, &peer_info->_peer_lst);

    if (result)
        g_variant_get(result, "(@a{sv})", &properties);

    _peer_info_properties_changed(self, peer_info, properties, TRUE);

    _starting_check_ready(self);
}

static void
_peer_info_add(NMSupplicantInterface *self, const char *object_path)
{
    NMSupplicantInterfacePrivate *priv        = NM_SUPPLICANT_INTERFACE_GET_PRIVATE(self);
    nm_auto_ref_string NMRefString *peer_path = NULL;
    NMSupplicantPeerInfo *          peer_info;

    peer_path = nm_ref_string_new(nm_dbus_path_not_empty(object_path));
    if (!peer_path)
        return;

    peer_info = g_hash_table_lookup(priv->peer_idx, &peer_path);

    if (peer_info) {
        peer_info->_peer_dirty = FALSE;
        return;
    }

    peer_info  = g_slice_new(NMSupplicantPeerInfo);
    *peer_info = (NMSupplicantPeerInfo){
        ._self             = self,
        .peer_path         = g_steal_pointer(&peer_path),
        ._init_cancellable = g_cancellable_new(),
    };
    c_list_link_tail(&priv->peer_initializing_lst_head, &peer_info->_peer_lst);
    g_hash_table_add(priv->peer_idx, peer_info);

    nm_dbus_connection_call_get_all(priv->dbus_connection,
                                    priv->name_owner->str,
                                    peer_info->peer_path->str,
                                    NM_WPAS_DBUS_IFACE_PEER,
                                    5000,
                                    peer_info->_init_cancellable,
                                    _peer_info_get_all_cb,
                                    peer_info);
}

static gboolean
_peer_info_remove(NMSupplicantInterface *self, NMRefString **p_peer_path)
{
    NMSupplicantInterfacePrivate *priv = NM_SUPPLICANT_INTERFACE_GET_PRIVATE(self);
    NMSupplicantPeerInfo *        peer_info;
    gpointer                      unused_but_required;

    if (!g_hash_table_steal_extended(priv->peer_idx,
                                     p_peer_path,
                                     (gpointer *) &peer_info,
                                     &unused_but_required))
        return FALSE;

    c_list_unlink(&peer_info->_peer_lst);
    if (!peer_info->_init_cancellable)
        _peer_info_changed_emit(self, peer_info, FALSE);
    _peer_info_destroy(peer_info);

    nm_assert_starting_has_pending_count(priv->starting_pending_count);

    return TRUE;
}

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

static void
set_state_down(NMSupplicantInterface *self,
               gboolean               force_remove_from_supplicant,
               const char *           reason)
{
    _nm_unused gs_unref_object NMSupplicantInterface *self_keep_alive = g_object_ref(self);
    NMSupplicantInterfacePrivate *priv = NM_SUPPLICANT_INTERFACE_GET_PRIVATE(self);
    NMSupplicantBssInfo *         bss_info;
    NMSupplicantPeerInfo *        peer_info;
    NMSupplicantInterfaceState    old_state;

    nm_assert(priv->state != NM_SUPPLICANT_INTERFACE_STATE_DOWN);
    nm_assert(!c_list_is_empty(&self->supp_lst));

    _LOGD("remove interface \"%s\" on %s (%s)%s",
          priv->object_path->str,
          priv->name_owner->str,
          reason,
          force_remove_from_supplicant ? " (remove in wpa_supplicant)" : "");

    old_state = priv->state;

    priv->state = NM_SUPPLICANT_INTERFACE_STATE_DOWN;

    _nm_supplicant_manager_unregister_interface(priv->supplicant_manager, self);

    nm_assert(c_list_is_empty(&self->supp_lst));

    if (force_remove_from_supplicant) {
        _nm_supplicant_manager_dbus_call_remove_interface(priv->supplicant_manager,
                                                          priv->name_owner->str,
                                                          priv->object_path->str);
    }

    _emit_signal_state(self, priv->state, old_state, 0);

    nm_clear_g_dbus_connection_signal(priv->dbus_connection, &priv->properties_changed_id);
    nm_clear_g_dbus_connection_signal(priv->dbus_connection, &priv->signal_id);
    nm_clear_g_dbus_connection_signal(priv->dbus_connection, &priv->bss_properties_changed_id);
    nm_clear_g_dbus_connection_signal(priv->dbus_connection, &priv->peer_properties_changed_id);
    nm_clear_g_dbus_connection_signal(priv->dbus_connection,
                                      &priv->p2p_group_properties_changed_id);

    nm_supplicant_interface_cancel_wps(self);

    if (priv->assoc_data) {
        gs_free_error GError *error = NULL;

        nm_utils_error_set_cancelled(&error, TRUE, "NMSupplicantInterface");
        assoc_return(self, error, "cancelled because supplicant interface is going down");
    }

    while (
        (bss_info =
             c_list_first_entry(&priv->bss_initializing_lst_head, NMSupplicantBssInfo, _bss_lst))) {
        g_hash_table_remove(priv->bss_idx, bss_info);
        _bss_info_destroy(bss_info);
    }
    while ((bss_info = c_list_first_entry(&priv->bss_lst_head, NMSupplicantBssInfo, _bss_lst))) {
        g_hash_table_remove(priv->bss_idx, bss_info);
        _bss_info_destroy(bss_info);
    }
    nm_assert(g_hash_table_size(priv->bss_idx) == 0);

    while ((peer_info = c_list_first_entry(&priv->peer_initializing_lst_head,
                                           NMSupplicantPeerInfo,
                                           _peer_lst))) {
        g_hash_table_remove(priv->peer_idx, peer_info);
        _peer_info_destroy(peer_info);
    }
    while (
        (peer_info = c_list_first_entry(&priv->peer_lst_head, NMSupplicantPeerInfo, _peer_lst))) {
        g_hash_table_remove(priv->peer_idx, peer_info);
        _peer_info_destroy(peer_info);
    }
    nm_assert(g_hash_table_size(priv->peer_idx) == 0);

    nm_clear_g_cancellable(&priv->main_cancellable);
    nm_clear_g_cancellable(&priv->p2p_group_properties_cancellable);

    nm_clear_pointer(&priv->p2p_group_path, nm_ref_string_unref);

    _remove_network(self);

    nm_clear_pointer(&priv->current_bss, nm_ref_string_unref);

    _notify_maybe_scanning(self);
}

static void
set_state(NMSupplicantInterface *self, NMSupplicantInterfaceState new_state)
{
    NMSupplicantInterfacePrivate *priv      = NM_SUPPLICANT_INTERFACE_GET_PRIVATE(self);
    NMSupplicantInterfaceState    old_state = priv->state;

    nm_assert(new_state > NM_SUPPLICANT_INTERFACE_STATE_STARTING);
    nm_assert(new_state < NM_SUPPLICANT_INTERFACE_STATE_DOWN);
    nm_assert(nm_supplicant_interface_state_is_operational(new_state));

    nm_assert(priv->state >= NM_SUPPLICANT_INTERFACE_STATE_STARTING);
    nm_assert(priv->state < NM_SUPPLICANT_INTERFACE_STATE_DOWN);

    if (new_state == priv->state)
        return;

    _LOGT("state: set state \"%s\" (was \"%s\")",
          nm_supplicant_interface_state_to_string(new_state),
          nm_supplicant_interface_state_to_string(priv->state));

    priv->state = new_state;

    _emit_signal_state(
        self,
        priv->state,
        old_state,
        priv->state != NM_SUPPLICANT_INTERFACE_STATE_DISCONNECTED ? 0u : priv->disconnect_reason);
}

NMRefString *
nm_supplicant_interface_get_current_bss(NMSupplicantInterface *self)
{
    g_return_val_if_fail(self != NULL, FALSE);

    return NM_SUPPLICANT_INTERFACE_GET_PRIVATE(self)->current_bss;
}

gboolean
nm_supplicant_interface_get_scanning(NMSupplicantInterface *self)
{
    g_return_val_if_fail(NM_IS_SUPPLICANT_INTERFACE(self), FALSE);

    return NM_SUPPLICANT_INTERFACE_GET_PRIVATE(self)->scanning_cached;
}

gint64
nm_supplicant_interface_get_last_scan(NMSupplicantInterface *self)
{
    g_return_val_if_fail(NM_IS_SUPPLICANT_INTERFACE(self), FALSE);

    return NM_SUPPLICANT_INTERFACE_GET_PRIVATE(self)->last_scan_msec;
}

#define MATCH_PROPERTY(p, n, v, t) (!strcmp(p, n) && g_variant_is_of_type(v, t))

static void
parse_capabilities(NMSupplicantInterface *self, GVariant *capabilities)
{
    NMSupplicantInterfacePrivate *priv                 = NM_SUPPLICANT_INTERFACE_GET_PRIVATE(self);
    const gboolean                old_prop_scan_active = priv->prop_scan_active;
    const gboolean                old_prop_scan_ssid   = priv->prop_scan_ssid;
    const guint32                 old_max_scan_ssids   = priv->max_scan_ssids;
    gboolean                      have_ft              = FALSE;
    gboolean                      have_sae             = FALSE;
    gint32                        max_scan_ssids;
    const char **                 array;

    nm_assert(capabilities && g_variant_is_of_type(capabilities, G_VARIANT_TYPE_VARDICT));

    if (g_variant_lookup(capabilities, "KeyMgmt", "^a&s", &array)) {
        have_ft  = g_strv_contains(array, "wpa-ft-psk");
        have_sae = g_strv_contains(array, "sae");
        g_free(array);
    }

    priv->iface_capabilities = NM_SUPPL_CAP_MASK_SET(priv->iface_capabilities,
                                                     NM_SUPPL_CAP_TYPE_FT,
                                                     have_ft ? NM_TERNARY_TRUE : NM_TERNARY_FALSE);
    priv->iface_capabilities = NM_SUPPL_CAP_MASK_SET(priv->iface_capabilities,
                                                     NM_SUPPL_CAP_TYPE_SAE,
                                                     have_sae ? NM_TERNARY_TRUE : NM_TERNARY_FALSE);

    if (g_variant_lookup(capabilities, "Modes", "^a&s", &array)) {
        /* Setting p2p_capable might toggle _prop_p2p_available_get(). However,
         * we don't need to check for a property changed notification, because
         * the caller did g_object_freeze_notify() and will perform the check. */
        priv->p2p_capable_property = g_strv_contains(array, "p2p");
        g_free(array);
    }

    if (g_variant_lookup(capabilities, "Scan", "^a&s", &array)) {
        const char **a;

        priv->prop_scan_active = FALSE;
        priv->prop_scan_ssid   = FALSE;
        for (a = array; *a; a++) {
            if (nm_streq(*a, "active"))
                priv->prop_scan_active = TRUE;
            else if (nm_streq(*a, "ssid"))
                priv->prop_scan_ssid = TRUE;
        }
        g_free(array);
    }

    if (g_variant_lookup(capabilities, "MaxScanSSID", "i", &max_scan_ssids)) {
        const gint32 WPAS_MAX_SCAN_SSIDS = 16;

        /* Even if supplicant claims that 20 SSIDs are supported, the Scan request
         * still only accepts WPAS_MAX_SCAN_SSIDS SSIDs. Otherwise, the D-Bus
         * request will be rejected with "fi.w1.wpa_supplicant1.InvalidArgs"
         * Body: ('Did not receive correct message arguments.', 'Too many ssids specified. Specify at most four')
         * */
        priv->max_scan_ssids = CLAMP(max_scan_ssids, 0, WPAS_MAX_SCAN_SSIDS);
    }

    if (old_max_scan_ssids != priv->max_scan_ssids || old_prop_scan_active != priv->prop_scan_active
        || old_prop_scan_ssid != priv->prop_scan_ssid) {
        _LOGD("supports %u scan SSIDs (scan: %cactive %cssid)",
              (guint32) priv->max_scan_ssids,
              priv->prop_scan_active ? '+' : '-',
              priv->prop_scan_ssid ? '+' : '-');
    }
}

static void
_starting_check_ready(NMSupplicantInterface *self)
{
    NMSupplicantInterfacePrivate *priv = NM_SUPPLICANT_INTERFACE_GET_PRIVATE(self);

    if (priv->state != NM_SUPPLICANT_INTERFACE_STATE_STARTING)
        return;

    if (priv->starting_pending_count > 0)
        return;

    if (!c_list_is_empty(&priv->bss_initializing_lst_head))
        return;

    if (!c_list_is_empty(&priv->peer_initializing_lst_head))
        return;

    if (priv->p2p_group_properties_cancellable)
        return;

    nm_assert(priv->state == NM_SUPPLICANT_INTERFACE_STATE_STARTING);

    if (!nm_supplicant_interface_state_is_operational(priv->supp_state)) {
        _LOGW("Supplicant state is unknown during initialization. Destroy the interface");
        set_state_down(self, TRUE, "failure to get valid interface state");
        return;
    }

    _LOGD("interface supported features:"
          " AP%c"
          " FT%c"
          " SAE%c"
          "",
          NM_SUPPL_CAP_TO_CHAR(priv->iface_capabilities, NM_SUPPL_CAP_TYPE_AP),
          NM_SUPPL_CAP_TO_CHAR(priv->iface_capabilities, NM_SUPPL_CAP_TYPE_FT),
          NM_SUPPL_CAP_TO_CHAR(priv->iface_capabilities, NM_SUPPL_CAP_TYPE_SAE));

    set_state(self, priv->supp_state);
}

static NMTernary
_get_capability(NMSupplicantInterfacePrivate *priv, NMSupplCapType type)
{
    NMTernary value;
    NMTernary iface_value;

    switch (type) {
    case NM_SUPPL_CAP_TYPE_AP:
        iface_value = NM_SUPPL_CAP_MASK_GET(priv->iface_capabilities, type);
        value       = NM_SUPPL_CAP_MASK_GET(priv->global_capabilities, type);
        value       = MAX(iface_value, value);
        break;
    case NM_SUPPL_CAP_TYPE_FT:
        value = NM_SUPPL_CAP_MASK_GET(priv->global_capabilities, type);
        if (value != NM_TERNARY_FALSE) {
            iface_value = NM_SUPPL_CAP_MASK_GET(priv->iface_capabilities, type);
            if (iface_value != NM_TERNARY_DEFAULT)
                value = iface_value;
        }
        break;
    case NM_SUPPL_CAP_TYPE_SAE:
        nm_assert(NM_SUPPL_CAP_MASK_GET(priv->global_capabilities, type) == NM_TERNARY_DEFAULT);
        value = NM_SUPPL_CAP_MASK_GET(priv->iface_capabilities, type);
        break;
    default:
        nm_assert(NM_SUPPL_CAP_MASK_GET(priv->iface_capabilities, type) == NM_TERNARY_DEFAULT);
        value = NM_SUPPL_CAP_MASK_GET(priv->global_capabilities, type);
        break;
    }
    return value;
}

NMTernary
nm_supplicant_interface_get_capability(NMSupplicantInterface *self, NMSupplCapType type)
{
    return _get_capability(NM_SUPPLICANT_INTERFACE_GET_PRIVATE(self), type);
}

NMSupplCapMask
nm_supplicant_interface_get_capabilities(NMSupplicantInterface *self)
{
    NMSupplicantInterfacePrivate *priv = NM_SUPPLICANT_INTERFACE_GET_PRIVATE(self);
    NMSupplCapMask                caps;

    caps = priv->global_capabilities;
    caps = NM_SUPPL_CAP_MASK_SET(caps,
                                 NM_SUPPL_CAP_TYPE_AP,
                                 _get_capability(priv, NM_SUPPL_CAP_TYPE_AP));
    caps = NM_SUPPL_CAP_MASK_SET(caps,
                                 NM_SUPPL_CAP_TYPE_FT,
                                 _get_capability(priv, NM_SUPPL_CAP_TYPE_FT));
    caps = NM_SUPPL_CAP_MASK_SET(caps,
                                 NM_SUPPL_CAP_TYPE_SAE,
                                 _get_capability(priv, NM_SUPPL_CAP_TYPE_SAE));

    nm_assert(!NM_FLAGS_ANY(priv->iface_capabilities,
                            ~(NM_SUPPL_CAP_MASK_T_AP_MASK | NM_SUPPL_CAP_MASK_T_FT_MASK
                              | NM_SUPPL_CAP_MASK_T_SAE_MASK)));

#if NM_MORE_ASSERTS > 10
    {
        NMSupplCapType type;

        for (type = 0; type < _NM_SUPPL_CAP_TYPE_NUM; type++)
            nm_assert(NM_SUPPL_CAP_MASK_GET(caps, type) == _get_capability(priv, type));
    }
#endif

    return caps;
}

static void
set_bridge_cb(GVariant *ret, GError *error, gpointer user_data)
{
    NMSupplicantInterface *self;
    NMLogLevel             level;
    gs_free const char *   bridge = NULL;

    nm_utils_user_data_unpack(user_data, &self, &bridge);

    if (nm_utils_error_is_cancelled(error))
        return;

    /* The supplicant supports writing the bridge property since
     * version 2.10. Before that version, trying to set the property
     * results in a InvalidArgs error.  Don't log a warning unless we
     * are trying to set a non-NULL bridge. */
    if (!error)
        level = LOGL_DEBUG;
    else if (bridge == NULL && g_error_matches(error, G_DBUS_ERROR, G_DBUS_ERROR_INVALID_ARGS)) {
        level = LOGL_DEBUG;
    } else
        level = LOGL_WARN;

    _NMLOG(level,
           "set bridge %s%s%s result: %s",
           NM_PRINT_FMT_QUOTE_STRING(bridge),
           error ? error->message : "success");
}

void
nm_supplicant_interface_set_bridge(NMSupplicantInterface *self, const char *bridge)
{
    NMSupplicantInterfacePrivate *priv = NM_SUPPLICANT_INTERFACE_GET_PRIVATE(self);

    _LOGT("set bridge %s%s%s", NM_PRINT_FMT_QUOTE_STRING(bridge));

    nm_dbus_connection_call_set(priv->dbus_connection,
                                priv->name_owner->str,
                                priv->object_path->str,
                                NM_WPAS_DBUS_IFACE_INTERFACE,
                                "BridgeIfname",
                                g_variant_new_string(bridge ?: ""),
                                DBUS_TIMEOUT_MSEC,
                                priv->main_cancellable,
                                set_bridge_cb,
                                nm_utils_user_data_pack(self, g_strdup(bridge)));
}

void
nm_supplicant_interface_set_global_capabilities(NMSupplicantInterface *self, NMSupplCapMask value)
{
    NMSupplicantInterfacePrivate *priv = NM_SUPPLICANT_INTERFACE_GET_PRIVATE(self);

    priv->global_capabilities = value;
}

NMSupplicantAuthState
nm_supplicant_interface_get_auth_state(NMSupplicantInterface *self)
{
    return NM_SUPPLICANT_INTERFACE_GET_PRIVATE(self)->auth_state;
}

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

static void
_p2p_group_properties_changed(NMSupplicantInterface *self, GVariant *properties)
{
    NMSupplicantInterfacePrivate *priv = NM_SUPPLICANT_INTERFACE_GET_PRIVATE(self);
    const char *                  s;

    if (!properties)
        priv->p2p_group_owner_property = FALSE;
    else if (g_variant_lookup(properties, "Role", "&s", &s))
        priv->p2p_group_owner_property = nm_streq(s, "GO");

    _notify_maybe_p2p_group(self);
}

static void
_p2p_group_properties_changed_cb(GDBusConnection *connection,
                                 const char *     sender_name,
                                 const char *     object_path,
                                 const char *     signal_interface_name,
                                 const char *     signal_name,
                                 GVariant *       parameters,
                                 gpointer         user_data)
{
    NMSupplicantInterface *       self            = NM_SUPPLICANT_INTERFACE(user_data);
    NMSupplicantInterfacePrivate *priv            = NM_SUPPLICANT_INTERFACE_GET_PRIVATE(self);
    gs_unref_variant GVariant *changed_properties = NULL;

    if (priv->p2p_group_properties_cancellable)
        return;
    if (!g_variant_is_of_type(parameters, G_VARIANT_TYPE("(sa{sv}as)")))
        return;

    g_variant_get(parameters, "(&s@a{sv}^a&s)", NULL, &changed_properties, NULL);

    _p2p_group_properties_changed(self, changed_properties);
}

static void
_p2p_group_properties_get_all_cb(GVariant *result, GError *error, gpointer user_data)
{
    NMSupplicantInterface *       self;
    NMSupplicantInterfacePrivate *priv;
    gs_unref_variant GVariant *properties = NULL;

    if (nm_utils_error_is_cancelled(error))
        return;

    self = NM_SUPPLICANT_INTERFACE(user_data);
    priv = NM_SUPPLICANT_INTERFACE_GET_PRIVATE(self);

    g_object_freeze_notify(G_OBJECT(self));

    nm_clear_g_cancellable(&priv->p2p_group_properties_cancellable);

    if (result)
        g_variant_get(result, "(@a{sv})", &properties);

    _p2p_group_properties_changed(self, properties);

    _starting_check_ready(self);

    _notify_maybe_p2p_group(self);

    g_object_thaw_notify(G_OBJECT(self));
}

static void
_p2p_group_set_path(NMSupplicantInterface *self, const char *path)
{
    NMSupplicantInterfacePrivate *priv         = NM_SUPPLICANT_INTERFACE_GET_PRIVATE(self);
    nm_auto_ref_string NMRefString *group_path = NULL;

    group_path = nm_ref_string_new(nm_dbus_path_not_empty(path));

    if (priv->p2p_group_path == group_path)
        return;

    nm_clear_g_dbus_connection_signal(priv->dbus_connection,
                                      &priv->p2p_group_properties_changed_id);
    nm_clear_g_cancellable(&priv->p2p_group_properties_cancellable);

    nm_ref_string_unref(priv->p2p_group_path);
    priv->p2p_group_path = g_steal_pointer(&group_path);

    if (priv->p2p_group_path) {
        priv->p2p_group_properties_cancellable = g_cancellable_new();
        priv->p2p_group_properties_changed_id =
            nm_dbus_connection_signal_subscribe_properties_changed(priv->dbus_connection,
                                                                   priv->name_owner->str,
                                                                   priv->p2p_group_path->str,
                                                                   NM_WPAS_DBUS_IFACE_GROUP,
                                                                   _p2p_group_properties_changed_cb,
                                                                   self,
                                                                   NULL);
        nm_dbus_connection_call_get_all(priv->dbus_connection,
                                        priv->name_owner->str,
                                        priv->p2p_group_path->str,
                                        NM_WPAS_DBUS_IFACE_GROUP,
                                        5000,
                                        priv->p2p_group_properties_cancellable,
                                        _p2p_group_properties_get_all_cb,
                                        self);
    }

    _notify(self, PROP_P2P_GROUP_PATH);
    _notify_maybe_p2p_group(self);

    nm_assert_starting_has_pending_count(priv->starting_pending_count);
}

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

static void
_wps_data_free(WpsData *wps_data, GDBusConnection *dbus_connection)
{
    nm_clear_g_dbus_connection_signal(dbus_connection, &wps_data->signal_id);
    nm_clear_g_cancellable(&wps_data->cancellable);
    g_free(wps_data->type);
    g_free(wps_data->pin);
    g_free(wps_data->bssid);
    nm_g_slice_free(wps_data);
}

static void
_wps_credentials_changed_cb(GDBusConnection *connection,
                            const char *     sender_name,
                            const char *     object_path,
                            const char *     signal_interface_name,
                            const char *     signal_name,
                            GVariant *       parameters,
                            gpointer         user_data)
{
    NMSupplicantInterface *self      = user_data;
    gs_unref_variant GVariant *props = NULL;

    if (!g_variant_is_of_type(parameters, G_VARIANT_TYPE("(a{sv})")))
        return;

    g_variant_get(parameters, "(@a{sv})", &props);

    _LOGT("wps: new credentials");
    g_signal_emit(self, signals[WPS_CREDENTIALS], 0, props);
}

static void
_wps_handle_start_cb(GObject *source, GAsyncResult *result, gpointer user_data)
{
    NMSupplicantInterface *self;
    WpsData *              wps_data;
    gs_unref_variant GVariant *res = NULL;
    gs_free_error GError *error    = NULL;

    res = g_dbus_connection_call_finish(G_DBUS_CONNECTION(source), result, &error);
    if (nm_utils_error_is_cancelled(error))
        return;

    wps_data = user_data;
    self     = wps_data->self;

    if (res)
        _LOGT("wps: started with success");
    else
        _LOGW("wps: start failed with %s", error->message);

    g_clear_object(&wps_data->cancellable);
    nm_clear_g_free(&wps_data->type);
    nm_clear_g_free(&wps_data->pin);
    nm_clear_g_free(&wps_data->bssid);
}

static void
_wps_handle_set_pc_cb(GVariant *res, GError *error, gpointer user_data)
{
    NMSupplicantInterface *       self;
    NMSupplicantInterfacePrivate *priv;
    WpsData *                     wps_data;
    GVariantBuilder               start_args;
    guint8                        bssid_buf[ETH_ALEN];

    if (nm_utils_error_is_cancelled(error))
        return;

    wps_data = user_data;
    self     = wps_data->self;
    priv     = NM_SUPPLICANT_INTERFACE_GET_PRIVATE(self);

    if (res)
        _LOGT("wps: ProcessCredentials successfully set, starting...");
    else
        _LOGW("wps: ProcessCredentials failed to set (%s), starting...", error->message);

    wps_data->signal_id = g_dbus_connection_signal_subscribe(priv->dbus_connection,
                                                             priv->name_owner->str,
                                                             NM_WPAS_DBUS_IFACE_INTERFACE_WPS,
                                                             "Credentials",
                                                             priv->object_path->str,
                                                             NULL,
                                                             G_DBUS_SIGNAL_FLAGS_NONE,
                                                             _wps_credentials_changed_cb,
                                                             self,
                                                             NULL);

    g_variant_builder_init(&start_args, G_VARIANT_TYPE_VARDICT);
    g_variant_builder_add(&start_args, "{sv}", "Role", g_variant_new_string("enrollee"));
    g_variant_builder_add(&start_args, "{sv}", "Type", g_variant_new_string(wps_data->type));
    if (wps_data->pin)
        g_variant_builder_add(&start_args, "{sv}", "Pin", g_variant_new_string(wps_data->pin));
    if (wps_data->bssid) {
        /* The BSSID is in fact not mandatory. If it is not set the supplicant would
         * enroll with any BSS in range. */
        if (!nm_utils_hwaddr_aton(wps_data->bssid, bssid_buf, sizeof(bssid_buf)))
            nm_assert_not_reached();
        g_variant_builder_add(&start_args,
                              "{sv}",
                              "Bssid",
                              nm_g_variant_new_ay(bssid_buf, ETH_ALEN));
    }

    wps_data->needs_cancelling = TRUE;
    if (!wps_data->cancellable)
        wps_data->cancellable = g_cancellable_new();

    _dbus_connection_call(self,
                          NM_WPAS_DBUS_IFACE_INTERFACE_WPS,
                          "Start",
                          g_variant_new("(a{sv})", &start_args),
                          G_VARIANT_TYPE("(a{sv})"),
                          G_DBUS_CALL_FLAGS_NONE,
                          5000,
                          wps_data->cancellable,
                          _wps_handle_start_cb,
                          wps_data);
}

static void
_wps_call_set_pc(NMSupplicantInterface *self, WpsData *wps_data)
{
    NMSupplicantInterfacePrivate *priv = NM_SUPPLICANT_INTERFACE_GET_PRIVATE(self);

    if (!wps_data->cancellable)
        wps_data->cancellable = g_cancellable_new();

    nm_dbus_connection_call_set(priv->dbus_connection,
                                priv->name_owner->str,
                                priv->object_path->str,
                                NM_WPAS_DBUS_IFACE_INTERFACE_WPS,
                                "ProcessCredentials",
                                g_variant_new_boolean(TRUE),
                                5000,
                                wps_data->cancellable,
                                _wps_handle_set_pc_cb,
                                wps_data);
}

static void
_wps_handle_cancel_cb(GObject *source, GAsyncResult *result, gpointer user_data)
{
    GDBusConnection *             dbus_connection = G_DBUS_CONNECTION(source);
    NMSupplicantInterface *       self;
    NMSupplicantInterfacePrivate *priv;
    WpsData *                     wps_data;
    gs_unref_variant GVariant *res = NULL;
    gs_free_error GError *error    = NULL;

    res = g_dbus_connection_call_finish(dbus_connection, result, &error);
    nm_assert(!nm_utils_error_is_cancelled(error));

    wps_data = user_data;
    self     = wps_data->self;

    if (!self) {
        _wps_data_free(wps_data, dbus_connection);
        if (res)
            _LOGT("wps: cancel completed successfully, after supplicant interface is gone");
        else
            _LOGW("wps: cancel failed (%s), after supplicant interface is gone", error->message);
        return;
    }

    priv = NM_SUPPLICANT_INTERFACE_GET_PRIVATE(self);

    wps_data->is_cancelling = FALSE;

    if (!wps_data->type) {
        priv->wps_data = NULL;
        _wps_data_free(wps_data, dbus_connection);
        if (res)
            _LOGT("wps: cancel completed successfully");
        else
            _LOGW("wps: cancel failed (%s)", error->message);
        return;
    }

    if (res)
        _LOGT("wps: cancel completed successfully, setting ProcessCredentials now...");
    else
        _LOGW("wps: cancel failed (%s), setting ProcessCredentials now...", error->message);

    _wps_call_set_pc(self, wps_data);
}

static void
_wps_start(NMSupplicantInterface *self, const char *type, const char *bssid, const char *pin)
{
    NMSupplicantInterfacePrivate *priv = NM_SUPPLICANT_INTERFACE_GET_PRIVATE(self);
    WpsData *                     wps_data;

    if (type)
        _LOGI("wps: type %s start...", type);

    wps_data = priv->wps_data;

    if (!wps_data) {
        if (!type)
            return;

        if (priv->state == NM_SUPPLICANT_INTERFACE_STATE_DOWN) {
            _LOGD("wps: interface is down. Cannot start with WPS");
            return;
        }

        wps_data  = g_slice_new(WpsData);
        *wps_data = (WpsData){
            .self  = self,
            .type  = g_strdup(type),
            .bssid = g_strdup(bssid),
            .pin   = g_strdup(pin),
        };
        priv->wps_data = wps_data;
    } else {
        g_free(wps_data->type);
        g_free(wps_data->bssid);
        g_free(wps_data->pin);
        wps_data->type  = g_strdup(type);
        wps_data->bssid = g_strdup(bssid);
        wps_data->pin   = g_strdup(pin);
    }

    if (wps_data->is_cancelling) {
        /* we wait for cancellation to complete. */
        return;
    }

    if (!type || wps_data->needs_cancelling) {
        _LOGT("wps: cancel %senrollment...", wps_data->needs_cancelling ? "previous " : "");

        wps_data->is_cancelling    = TRUE;
        wps_data->needs_cancelling = FALSE;
        nm_clear_g_cancellable(&wps_data->cancellable);
        nm_clear_g_dbus_connection_signal(priv->dbus_connection, &wps_data->signal_id);

        _dbus_connection_call(self,
                              NM_WPAS_DBUS_IFACE_INTERFACE_WPS,
                              "Cancel",
                              NULL,
                              G_VARIANT_TYPE("()"),
                              G_DBUS_CALL_FLAGS_NONE,
                              5000,
                              NULL,
                              _wps_handle_cancel_cb,
                              wps_data);
        return;
    }

    _LOGT("wps: setting ProcessCredentials...");
    _wps_call_set_pc(self, wps_data);
}

void
nm_supplicant_interface_enroll_wps(NMSupplicantInterface *self,
                                   const char *           type,
                                   const char *           bssid,
                                   const char *           pin)
{
    _wps_start(self, type, bssid, pin);
}

void
nm_supplicant_interface_cancel_wps(NMSupplicantInterface *self)
{
    _wps_start(self, NULL, NULL, NULL);
}

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

static void
iface_introspect_cb(GObject *source, GAsyncResult *result, gpointer user_data)
{
    NMSupplicantInterface *       self;
    NMSupplicantInterfacePrivate *priv;
    gs_unref_variant GVariant *res = NULL;
    gs_free_error GError *error    = NULL;
    const char *          data;
    NMTernary             value;

    res = g_dbus_connection_call_finish(G_DBUS_CONNECTION(source), result, &error);
    if (nm_utils_error_is_cancelled(error))
        return;

    self = NM_SUPPLICANT_INTERFACE(user_data);
    priv = NM_SUPPLICANT_INTERFACE_GET_PRIVATE(self);

    nm_assert(NM_SUPPL_CAP_MASK_GET(priv->global_capabilities, NM_SUPPL_CAP_TYPE_AP)
              == NM_TERNARY_DEFAULT);

    value = NM_TERNARY_DEFAULT;
    if (res) {
        g_variant_get(res, "(&s)", &data);

        /* The ProbeRequest method only exists if AP mode has been enabled */
        value = strstr(data, "ProbeRequest") ? NM_TERNARY_TRUE : NM_TERNARY_FALSE;
    }

    priv->iface_capabilities =
        NM_SUPPL_CAP_MASK_SET(priv->iface_capabilities, NM_SUPPL_CAP_TYPE_AP, value);

    priv->starting_pending_count--;
    _starting_check_ready(self);
}

static void
_properties_changed_main(NMSupplicantInterface *self, GVariant *properties)
{
    NMSupplicantInterfacePrivate *priv = NM_SUPPLICANT_INTERFACE_GET_PRIVATE(self);
    const char **                 v_strv;
    const char *                  v_s;
    gboolean                      v_b;
    gint32                        v_i32;
    GVariant *                    v_v;
    gboolean                      do_log_driver_info    = FALSE;
    gboolean                      do_set_state          = FALSE;
    gboolean                      do_notify_current_bss = FALSE;

    nm_assert(properties || g_variant_is_of_type(properties, G_VARIANT_TYPE("a{sv}")));

    v_v = g_variant_lookup_value(properties, "Capabilities", G_VARIANT_TYPE_VARDICT);
    if (v_v) {
        parse_capabilities(self, v_v);
        g_variant_unref(v_v);
    }

    if (nm_g_variant_lookup(properties, "Scanning", "b", &v_b)) {
        if (priv->scanning_property != (!!v_b)) {
            _LOGT("scanning: %s (plain property)", v_b ? "yes" : "no");
            priv->scanning_property = v_b;
        }
    }

    if (nm_g_variant_lookup(properties, "Ifname", "&s", &v_s)) {
        if (nm_utils_strdup_reset(&priv->ifname, v_s))
            do_log_driver_info = TRUE;
    }
    if (nm_g_variant_lookup(properties, "Driver", "&s", &v_s)) {
        if (nm_utils_strdup_reset(&priv->driver, v_s))
            do_log_driver_info = TRUE;
    }

    if (nm_g_variant_lookup(properties, "DisconnectReason", "i", &v_i32)) {
        /* Disconnect reason is currently only given for deauthentication events,
         * not disassociation; currently they are IEEE 802.11 "reason codes",
         * defined by (IEEE 802.11-2007, 7.3.1.7, Table 7-22).  Any locally caused
         * deauthentication will be negative, while authentications caused by the
         * AP will be positive.
         */
        priv->disconnect_reason = v_i32;
    }

    if (nm_g_variant_lookup(properties, "State", "&s", &v_s)) {
        NMSupplicantInterfaceState state;

        state = wpas_state_string_to_enum(v_s);
        if (state == NM_SUPPLICANT_INTERFACE_STATE_INVALID)
            _LOGT("state: ignore unknown supplicant state '%s' (is %s, plain property)",
                  v_s,
                  nm_supplicant_interface_state_to_string(priv->supp_state));
        else if (priv->supp_state != state) {
            _LOGT("state: %s (was %s, plain property)",
                  nm_supplicant_interface_state_to_string(state),
                  nm_supplicant_interface_state_to_string(priv->supp_state));
            priv->supp_state = state;
            if (priv->state > NM_SUPPLICANT_INTERFACE_STATE_STARTING) {
                /* Only transition to actual wpa_supplicant interface states (ie,
                 * anything > STARTING) after the NMSupplicantInterface has had a
                 * chance to initialize, which is signalled by entering the STARTING
                 * state.
                 */
                do_set_state = TRUE;
            }
        }
    }

    if (nm_g_variant_lookup(properties, "CurrentBSS", "&o", &v_s)) {
        v_s = nm_dbus_path_not_empty(v_s);
        if (!nm_ref_string_equal_str(priv->current_bss, v_s)) {
            nm_ref_string_unref(priv->current_bss);
            priv->current_bss     = nm_ref_string_new(v_s);
            do_notify_current_bss = TRUE;
        }
    }

    if (nm_g_variant_lookup(properties, "ApIsolate", "&s", &v_s))
        priv->ap_isolate_supported = TRUE;

    if (do_log_driver_info) {
        _LOGD("supplicant interface for ifindex=%d, ifname=%s%s%s, driver=%s%s%s (requested %s)",
              priv->ifindex,
              NM_PRINT_FMT_QUOTE_STRING(priv->ifname),
              NM_PRINT_FMT_QUOTE_STRING(priv->driver),
              nm_supplicant_driver_to_string(priv->requested_driver));
    }

    if (nm_g_variant_lookup(properties, "BSSs", "^a&o", &v_strv)) {
        NMSupplicantBssInfo *bss_info;
        NMSupplicantBssInfo *bss_info_safe;
        const char **        iter;

        c_list_for_each_entry (bss_info, &priv->bss_lst_head, _bss_lst)
            bss_info->_bss_dirty = TRUE;
        c_list_for_each_entry (bss_info, &priv->bss_initializing_lst_head, _bss_lst)
            bss_info->_bss_dirty = TRUE;

        for (iter = v_strv; *iter; iter++)
            _bss_info_add(self, *iter);

        g_free(v_strv);

        c_list_for_each_entry_safe (bss_info,
                                    bss_info_safe,
                                    &priv->bss_initializing_lst_head,
                                    _bss_lst) {
            if (bss_info->_bss_dirty)
                _bss_info_remove(self, &bss_info->bss_path);
        }
        c_list_for_each_entry_safe (bss_info, bss_info_safe, &priv->bss_lst_head, _bss_lst) {
            if (bss_info->_bss_dirty)
                _bss_info_remove(self, &bss_info->bss_path);
        }
    }

    if (do_notify_current_bss)
        _notify(self, PROP_CURRENT_BSS);

    if (do_set_state)
        set_state(self, priv->supp_state);

    _notify_maybe_scanning(self);
}

static void
_properties_changed_p2p_device(NMSupplicantInterface *self, GVariant *properties)
{
    NMSupplicantInterfacePrivate *priv = NM_SUPPLICANT_INTERFACE_GET_PRIVATE(self);
    const char **                 v_strv;
    const char *                  v_s;

    nm_assert(!properties || g_variant_is_of_type(properties, G_VARIANT_TYPE("a{sv}")));

    if (nm_g_variant_lookup(properties, "Peers", "^a&o", &v_strv)) {
        NMSupplicantPeerInfo *peer_info;
        NMSupplicantPeerInfo *peer_info_safe;
        const char *const *   iter;

        c_list_for_each_entry (peer_info, &priv->peer_lst_head, _peer_lst)
            peer_info->_peer_dirty = TRUE;
        c_list_for_each_entry (peer_info, &priv->peer_initializing_lst_head, _peer_lst)
            peer_info->_peer_dirty = TRUE;

        for (iter = v_strv; *iter; iter++)
            _peer_info_add(self, *iter);

        g_free(v_strv);

        c_list_for_each_entry_safe (peer_info,
                                    peer_info_safe,
                                    &priv->peer_initializing_lst_head,
                                    _peer_lst) {
            if (peer_info->_peer_dirty)
                _peer_info_remove(self, &peer_info->peer_path);
        }
        c_list_for_each_entry_safe (peer_info, peer_info_safe, &priv->peer_lst_head, _peer_lst) {
            if (peer_info->_peer_dirty)
                _peer_info_remove(self, &peer_info->peer_path);
        }
    }

    if (nm_g_variant_lookup(properties, "Group", "&o", &v_s))
        _p2p_group_set_path(self, v_s);
}

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

static void
assoc_return(NMSupplicantInterface *self, GError *error, const char *message)
{
    NMSupplicantInterfacePrivate *priv = NM_SUPPLICANT_INTERFACE_GET_PRIVATE(self);
    AssocData *                   assoc_data;

    assoc_data = g_steal_pointer(&priv->assoc_data);
    if (!assoc_data)
        return;

    if (error) {
        g_dbus_error_strip_remote_error(error);
        _LOGW("assoc[" NM_HASH_OBFUSCATE_PTR_FMT "]: %s: %s",
              NM_HASH_OBFUSCATE_PTR(assoc_data),
              message,
              error->message);
    } else {
        _LOGD("assoc[" NM_HASH_OBFUSCATE_PTR_FMT "]: association request successful",
              NM_HASH_OBFUSCATE_PTR(assoc_data));
    }

    if (assoc_data->add_network_data) {
        /* signal that this request already completed */
        assoc_data->add_network_data->assoc_data = NULL;
    }

    nm_clear_g_source(&assoc_data->fail_on_idle_id);
    nm_clear_g_cancellable(&assoc_data->cancellable);

    if (assoc_data->callback)
        assoc_data->callback(self, error, assoc_data->user_data);

    g_object_unref(assoc_data->cfg);
    g_slice_free(AssocData, assoc_data);
}

void
nm_supplicant_interface_disconnect(NMSupplicantInterface *self)
{
    NMSupplicantInterfacePrivate *priv;

    g_return_if_fail(NM_IS_SUPPLICANT_INTERFACE(self));

    priv = NM_SUPPLICANT_INTERFACE_GET_PRIVATE(self);

    /* Disconnect from the current AP */
    if ((priv->state >= NM_SUPPLICANT_INTERFACE_STATE_SCANNING)
        && (priv->state <= NM_SUPPLICANT_INTERFACE_STATE_COMPLETED)) {
        _dbus_connection_call_simple(self,
                                     NM_WPAS_DBUS_IFACE_INTERFACE,
                                     "Disconnect",
                                     NULL,
                                     G_VARIANT_TYPE("()"),
                                     "disconnect");
    }

    _remove_network(self);

    /* Cancel any WPS enrollment, if any */
    nm_supplicant_interface_cancel_wps(self);

    /* Cancel all pending calls related to a prior connection attempt */
    if (priv->assoc_data) {
        gs_free_error GError *error = NULL;

        nm_utils_error_set_cancelled(&error, FALSE, "NMSupplicantInterface");
        assoc_return(self, error, "abort due to disconnect");
    }
}

static void
disconnect_cb(GObject *source, GAsyncResult *result, gpointer user_data)
{
    gs_unref_object NMSupplicantInterface *self = NULL;
    gs_unref_variant GVariant *res              = NULL;
    gs_free_error GError *            error     = NULL;
    NMSupplicantInterfaceDisconnectCb callback;
    gpointer                          callback_user_data;

    nm_utils_user_data_unpack(user_data, &self, &callback, &callback_user_data);

    res = g_dbus_connection_call_finish(G_DBUS_CONNECTION(source), result, &error);

    if (!res && !strstr(error->message, "fi.w1.wpa_supplicant1.NotConnected")) {
        /* an already disconnected interface is not an error*/
        g_clear_error(&error);
    }

    callback(self, error, callback_user_data);
}

void
nm_supplicant_interface_disconnect_async(NMSupplicantInterface *           self,
                                         GCancellable *                    cancellable,
                                         NMSupplicantInterfaceDisconnectCb callback,
                                         gpointer                          user_data)
{
    g_return_if_fail(NM_IS_SUPPLICANT_INTERFACE(self));
    g_return_if_fail(callback);

    _dbus_connection_call(self,
                          NM_WPAS_DBUS_IFACE_INTERFACE,
                          "Disconnect",
                          NULL,
                          G_VARIANT_TYPE("()"),
                          G_DBUS_CALL_FLAGS_NONE,
                          DBUS_TIMEOUT_MSEC,
                          cancellable,
                          disconnect_cb,
                          nm_utils_user_data_pack(g_object_ref(self), callback, user_data));
}

static void
assoc_select_network_cb(GObject *source, GAsyncResult *result, gpointer user_data)
{
    NMSupplicantInterface *self;
    gs_unref_variant GVariant *res = NULL;
    gs_free_error GError *error    = NULL;

    res = g_dbus_connection_call_finish(G_DBUS_CONNECTION(source), result, &error);
    if (nm_utils_error_is_cancelled(error))
        return;

    self = NM_SUPPLICANT_INTERFACE(user_data);
    if (error)
        assoc_return(self, error, "failure to select network config");
    else
        assoc_return(self, NULL, NULL);
}

static void
assoc_call_select_network(NMSupplicantInterface *self)
{
    NMSupplicantInterfacePrivate *priv = NM_SUPPLICANT_INTERFACE_GET_PRIVATE(self);

    _dbus_connection_call(self,
                          NM_WPAS_DBUS_IFACE_INTERFACE,
                          "SelectNetwork",
                          g_variant_new("(o)", priv->net_path),
                          G_VARIANT_TYPE("()"),
                          G_DBUS_CALL_FLAGS_NONE,
                          DBUS_TIMEOUT_MSEC,
                          priv->assoc_data->cancellable,
                          assoc_select_network_cb,
                          self);
}

static void
assoc_add_blob_cb(GObject *source, GAsyncResult *result, gpointer user_data)
{
    NMSupplicantInterface *       self;
    NMSupplicantInterfacePrivate *priv;
    gs_unref_variant GVariant *res = NULL;
    gs_free_error GError *error    = NULL;

    res = g_dbus_connection_call_finish(G_DBUS_CONNECTION(source), result, &error);
    if (nm_utils_error_is_cancelled(error))
        return;

    self = NM_SUPPLICANT_INTERFACE(user_data);
    priv = NM_SUPPLICANT_INTERFACE_GET_PRIVATE(self);

    if (error) {
        assoc_return(self, error, "failure to set network certificates");
        return;
    }

    priv->assoc_data->blobs_left--;
    _LOGT("assoc[" NM_HASH_OBFUSCATE_PTR_FMT "]: blob added (%u left)",
          NM_HASH_OBFUSCATE_PTR(priv->assoc_data),
          priv->assoc_data->blobs_left);
    if (priv->assoc_data->blobs_left == 0)
        assoc_call_select_network(self);
}

static void
assoc_add_network_cb(GObject *source, GAsyncResult *result, gpointer user_data)
{
    AddNetworkData *              add_network_data = user_data;
    AssocData *                   assoc_data;
    NMSupplicantInterface *       self;
    NMSupplicantInterfacePrivate *priv;
    gs_unref_variant GVariant *res = NULL;
    gs_free_error GError *error    = NULL;
    GHashTable *          blobs;
    GHashTableIter        iter;
    const char *          blob_name;
    GBytes *              blob_data;
    nm_auto_ref_string NMRefString *name_owner  = NULL;
    nm_auto_ref_string NMRefString *object_path = NULL;

    g_clear_object(&add_network_data->shutdown_wait_obj);

    assoc_data = add_network_data->assoc_data;
    if (assoc_data)
        assoc_data->add_network_data = NULL;
    name_owner  = g_steal_pointer(&add_network_data->name_owner);
    object_path = g_steal_pointer(&add_network_data->object_path);
    nm_g_slice_free(add_network_data);

    res = g_dbus_connection_call_finish(G_DBUS_CONNECTION(source), result, &error);

    if (!assoc_data) {
        if (!error) {
            const char *net_path;

            /* the assoc-request was already cancelled, but the AddNetwork request succeeded.
             * Cleanup the created network.
             *
             * This cleanup action does not work when NetworkManager is about to exit
             * and leaves the mainloop. During program shutdown, we may orphan networks. */
            g_variant_get(res, "(&o)", &net_path);
            g_dbus_connection_call(G_DBUS_CONNECTION(source),
                                   name_owner->str,
                                   object_path->str,
                                   NM_WPAS_DBUS_IFACE_INTERFACE,
                                   "RemoveNetwork",
                                   g_variant_new("(o)", net_path),
                                   G_VARIANT_TYPE("()"),
                                   G_DBUS_CALL_FLAGS_NONE,
                                   DBUS_TIMEOUT_MSEC,
                                   NULL,
                                   NULL,
                                   NULL);
        }
        return;
    }

    self = NM_SUPPLICANT_INTERFACE(assoc_data->self);
    priv = NM_SUPPLICANT_INTERFACE_GET_PRIVATE(self);

    if (error) {
        assoc_return(self, error, "failure to add network");
        return;
    }

    nm_assert(!priv->net_path);
    g_variant_get(res, "(o)", &priv->net_path);

    /* Send blobs first; otherwise jump to selecting the network */
    blobs                        = nm_supplicant_config_get_blobs(priv->assoc_data->cfg);
    priv->assoc_data->blobs_left = blobs ? g_hash_table_size(blobs) : 0u;

    _LOGT("assoc[" NM_HASH_OBFUSCATE_PTR_FMT "]: network added (%s) (%u blobs left)",
          NM_HASH_OBFUSCATE_PTR(priv->assoc_data),
          priv->net_path,
          priv->assoc_data->blobs_left);

    if (priv->assoc_data->blobs_left == 0) {
        assoc_call_select_network(self);
        return;
    }

    g_hash_table_iter_init(&iter, blobs);
    while (g_hash_table_iter_next(&iter, (gpointer) &blob_name, (gpointer) &blob_data)) {
        _dbus_connection_call(
            self,
            NM_WPAS_DBUS_IFACE_INTERFACE,
            "AddBlob",
            g_variant_new("(s@ay)", blob_name, nm_utils_gbytes_to_variant_ay(blob_data)),
            G_VARIANT_TYPE("()"),
            G_DBUS_CALL_FLAGS_NONE,
            DBUS_TIMEOUT_MSEC,
            priv->assoc_data->cancellable,
            assoc_add_blob_cb,
            self);
    }
}

static void
add_network(NMSupplicantInterface *self)
{
    NMSupplicantInterfacePrivate *priv;
    AddNetworkData *              add_network_data;

    priv = NM_SUPPLICANT_INTERFACE_GET_PRIVATE(self);

    /* the association does not keep @self alive. We want to be able to remove
     * the network again, even if @self is already gone. Hence, track the data
     * separately.
     *
     * For that we also have a shutdown_wait_obj so that on exit we still wait
     * to handle the response. */
    add_network_data  = g_slice_new(AddNetworkData);
    *add_network_data = (AddNetworkData){
        .assoc_data        = priv->assoc_data,
        .name_owner        = nm_ref_string_ref(priv->name_owner),
        .object_path       = nm_ref_string_ref(priv->object_path),
        .shutdown_wait_obj = g_object_new(G_TYPE_OBJECT, NULL),
    };
    nm_shutdown_wait_obj_register_object(add_network_data->shutdown_wait_obj,
                                         "supplicant-add-network");
    priv->assoc_data->add_network_data = add_network_data;

    _dbus_connection_call(
        self,
        NM_WPAS_DBUS_IFACE_INTERFACE,
        "AddNetwork",
        g_variant_new("(@a{sv})", nm_supplicant_config_to_variant(priv->assoc_data->cfg)),
        G_VARIANT_TYPE("(o)"),
        G_DBUS_CALL_FLAGS_NONE,
        DBUS_TIMEOUT_MSEC,
        NULL,
        assoc_add_network_cb,
        add_network_data);
}

static void
assoc_set_ap_isolation(GVariant *ret, GError *error, gpointer user_data)
{
    NMSupplicantInterface *       self;
    NMSupplicantInterfacePrivate *priv;
    gboolean                      value;

    if (nm_utils_error_is_cancelled(error))
        return;

    self = NM_SUPPLICANT_INTERFACE(user_data);
    priv = NM_SUPPLICANT_INTERFACE_GET_PRIVATE(self);

    if (error) {
        assoc_return(self, error, "failure to set AP isolation");
        return;
    }

    value = nm_supplicant_config_get_ap_isolation(priv->assoc_data->cfg);
    _LOGT("assoc[" NM_HASH_OBFUSCATE_PTR_FMT "]: interface AP isolation set to %d",
          NM_HASH_OBFUSCATE_PTR(priv->assoc_data),
          value);

    priv->ap_isolate_needs_reset = value;

    nm_assert(priv->assoc_data->calls_left > 0);
    if (--priv->assoc_data->calls_left == 0)
        add_network(self);
}

static void
assoc_set_ap_scan_cb(GVariant *ret, GError *error, gpointer user_data)
{
    NMSupplicantInterface *       self;
    NMSupplicantInterfacePrivate *priv;

    if (nm_utils_error_is_cancelled(error))
        return;

    self = NM_SUPPLICANT_INTERFACE(user_data);
    priv = NM_SUPPLICANT_INTERFACE_GET_PRIVATE(self);

    if (error) {
        assoc_return(self, error, "failure to set AP scan mode");
        return;
    }

    _LOGT("assoc[" NM_HASH_OBFUSCATE_PTR_FMT "]: interface ap_scan set to %d",
          NM_HASH_OBFUSCATE_PTR(priv->assoc_data),
          nm_supplicant_config_get_ap_scan(priv->assoc_data->cfg));

    nm_assert(priv->assoc_data->calls_left > 0);
    if (--priv->assoc_data->calls_left == 0)
        add_network(self);
}

static gboolean
assoc_fail_on_idle_cb(gpointer user_data)
{
    NMSupplicantInterface *       self = user_data;
    NMSupplicantInterfacePrivate *priv = NM_SUPPLICANT_INTERFACE_GET_PRIVATE(self);
    gs_free_error GError *error        = NULL;

    priv->assoc_data->fail_on_idle_id = 0;
    g_set_error(&error,
                NM_SUPPLICANT_ERROR,
                NM_SUPPLICANT_ERROR_CONFIG,
                "EAP-FAST is not supported by the supplicant");
    assoc_return(self, error, "failure due to missing supplicant support");
    return G_SOURCE_REMOVE;
}

/**
 * nm_supplicant_interface_assoc:
 * @self: the supplicant interface instance
 * @cfg: the configuration with the data for the association
 * @callback: callback invoked when the association completes or fails.
 * @user_data: data for the callback.
 *
 * Calls AddNetwork and SelectNetwork to start associating according to @cfg.
 *
 * The callback is invoked exactly once (always) and always asynchronously.
 * The pending association can be aborted via nm_supplicant_interface_disconnect()
 * or by destroying @self. In that case, the @callback is invoked synchronously with
 * an error reason indicating cancellation/disposing (see nm_utils_error_is_cancelled()).
 */
void
nm_supplicant_interface_assoc(NMSupplicantInterface *      self,
                              NMSupplicantConfig *         cfg,
                              NMSupplicantInterfaceAssocCb callback,
                              gpointer                     user_data)
{
    NMSupplicantInterfacePrivate *priv;
    AssocData *                   assoc_data;
    gboolean                      ap_isolation;

    g_return_if_fail(NM_IS_SUPPLICANT_INTERFACE(self));
    g_return_if_fail(NM_IS_SUPPLICANT_CONFIG(cfg));

    priv = NM_SUPPLICANT_INTERFACE_GET_PRIVATE(self);

    nm_supplicant_interface_disconnect(self);

    assoc_data  = g_slice_new(AssocData);
    *assoc_data = (AssocData){
        .self      = self,
        .cfg       = g_object_ref(cfg),
        .callback  = callback,
        .user_data = user_data,
    };

    priv->assoc_data = assoc_data;

    _LOGD("assoc[" NM_HASH_OBFUSCATE_PTR_FMT "]: starting association...",
          NM_HASH_OBFUSCATE_PTR(assoc_data));

    if (_get_capability(priv, NM_SUPPL_CAP_TYPE_FAST) == NM_TERNARY_FALSE
        && nm_supplicant_config_fast_required(cfg)) {
        /* Make sure the supplicant supports EAP-FAST before trying to send
         * it an EAP-FAST configuration.
         */
        assoc_data->fail_on_idle_id = g_idle_add(assoc_fail_on_idle_cb, self);
        return;
    }

    assoc_data->cancellable = g_cancellable_new();
    assoc_data->calls_left++;
    nm_dbus_connection_call_set(
        priv->dbus_connection,
        priv->name_owner->str,
        priv->object_path->str,
        NM_WPAS_DBUS_IFACE_INTERFACE,
        "ApScan",
        g_variant_new_uint32(nm_supplicant_config_get_ap_scan(priv->assoc_data->cfg)),
        DBUS_TIMEOUT_MSEC,
        assoc_data->cancellable,
        assoc_set_ap_scan_cb,
        self);

    ap_isolation = nm_supplicant_config_get_ap_isolation(priv->assoc_data->cfg);
    if (!priv->ap_isolate_supported) {
        if (ap_isolation) {
            _LOGW("assoc[" NM_HASH_OBFUSCATE_PTR_FMT
                  "]: requested AP isolation but the supplicant doesn't support it",
                  NM_HASH_OBFUSCATE_PTR(assoc_data));
        }
    } else {
        assoc_data->calls_left++;
        /* It would be smarter to change the property only when necessary.
         * However, wpa_supplicant doesn't send the PropertiesChanged
         * signal for ApIsolate, and so to know the current value we would
         * need first a Get call. It seems simpler to just set the value
         * we want. */
        nm_dbus_connection_call_set(priv->dbus_connection,
                                    priv->name_owner->str,
                                    priv->object_path->str,
                                    NM_WPAS_DBUS_IFACE_INTERFACE,
                                    "ApIsolate",
                                    g_variant_new_string(ap_isolation ? "1" : "0"),
                                    DBUS_TIMEOUT_MSEC,
                                    assoc_data->cancellable,
                                    assoc_set_ap_isolation,
                                    self);
    }
}

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

typedef struct {
    NMSupplicantInterface *                  self;
    GCancellable *                           cancellable;
    NMSupplicantInterfaceRequestScanCallback callback;
    gpointer                                 user_data;
} ScanRequestData;

static void
scan_request_cb(GObject *source, GAsyncResult *result, gpointer user_data)
{
    gs_unref_object NMSupplicantInterface *self_keep_alive = NULL;
    NMSupplicantInterface *                self;
    gs_unref_variant GVariant *res  = NULL;
    gs_free_error GError *error     = NULL;
    ScanRequestData *     data      = user_data;
    gboolean              cancelled = FALSE;

    res = g_dbus_connection_call_finish(G_DBUS_CONNECTION(source), result, &error);
    if (nm_utils_error_is_cancelled(error)) {
        if (!data->callback) {
            /* the self instance was not kept alive. We also must not touch it. Return. */
            nm_g_object_unref(data->cancellable);
            nm_g_slice_free(data);
            return;
        }
        cancelled = TRUE;
    }

    self = data->self;
    if (data->callback) {
        /* the self instance was kept alive. Balance the reference count. */
        self_keep_alive = self;
    }

    /* we don't propagate the error/success. That is, because either answer is not
     * reliable. What is important to us is whether the request completed, and
     * the current nm_supplicant_interface_get_scanning() state. */
    if (cancelled)
        _LOGD("request-scan: request cancelled");
    else {
        if (error) {
            if (_nm_dbus_error_has_name(error, "fi.w1.wpa_supplicant1.Interface.ScanError"))
                _LOGD("request-scan: could not get scan request result: %s", error->message);
            else {
                g_dbus_error_strip_remote_error(error);
                _LOGW("request-scan: could not get scan request result: %s", error->message);
            }
        } else
            _LOGT("request-scan: request scanning success");
    }

    if (data->callback)
        data->callback(self, data->cancellable, data->user_data);

    nm_g_object_unref(data->cancellable);
    nm_g_slice_free(data);
}

void
nm_supplicant_interface_request_scan(NMSupplicantInterface *                  self,
                                     GBytes *const *                          ssids,
                                     guint                                    ssids_len,
                                     GCancellable *                           cancellable,
                                     NMSupplicantInterfaceRequestScanCallback callback,
                                     gpointer                                 user_data)
{
    NMSupplicantInterfacePrivate *priv;
    GVariantBuilder               builder;
    ScanRequestData *             data;
    guint                         i;

    g_return_if_fail(NM_IS_SUPPLICANT_INTERFACE(self));

    nm_assert((!cancellable && !callback) || (G_IS_CANCELLABLE(cancellable) && callback));

    priv = NM_SUPPLICANT_INTERFACE_GET_PRIVATE(self);

    _LOGT("request-scan: request scanning (%u ssids)...", ssids_len);

    g_variant_builder_init(&builder, G_VARIANT_TYPE_VARDICT);
    g_variant_builder_add(&builder, "{sv}", "Type", g_variant_new_string("active"));
    g_variant_builder_add(&builder, "{sv}", "AllowRoam", g_variant_new_boolean(FALSE));
    if (ssids_len > 0) {
        GVariantBuilder ssids_builder;

        g_variant_builder_init(&ssids_builder, G_VARIANT_TYPE_BYTESTRING_ARRAY);
        for (i = 0; i < ssids_len; i++) {
            nm_assert(ssids[i]);
            g_variant_builder_add(&ssids_builder, "@ay", nm_utils_gbytes_to_variant_ay(ssids[i]));
        }
        g_variant_builder_add(&builder, "{sv}", "SSIDs", g_variant_builder_end(&ssids_builder));
    }

    data  = g_slice_new(ScanRequestData);
    *data = (ScanRequestData){
        .self        = self,
        .callback    = callback,
        .user_data   = user_data,
        .cancellable = nm_g_object_ref(cancellable),
    };

    if (callback) {
        /* A callback was provided. This keeps @self alive. The caller
         * must provide a cancellable as the caller must never leave an asynchronous
         * operation pending indefinitely. */
        nm_assert(G_IS_CANCELLABLE(cancellable));
        g_object_ref(self);
    } else {
        /* We don't keep @self alive, and we don't accept a cancellable either. */
        nm_assert(!cancellable);
        cancellable = priv->main_cancellable;
    }

    _dbus_connection_call(self,
                          NM_WPAS_DBUS_IFACE_INTERFACE,
                          "Scan",
                          g_variant_new("(a{sv})", &builder),
                          G_VARIANT_TYPE("()"),
                          G_DBUS_CALL_FLAGS_NONE,
                          DBUS_TIMEOUT_MSEC,
                          cancellable,
                          scan_request_cb,
                          data);
}

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

NMSupplicantInterfaceState
nm_supplicant_interface_get_state(NMSupplicantInterface *self)
{
    g_return_val_if_fail(NM_IS_SUPPLICANT_INTERFACE(self), NM_SUPPLICANT_INTERFACE_STATE_DOWN);

    return NM_SUPPLICANT_INTERFACE_GET_PRIVATE(self)->state;
}

void
_nm_supplicant_interface_set_state_down(NMSupplicantInterface *self,
                                        gboolean               force_remove_from_supplicant,
                                        const char *           reason)
{
    set_state_down(self, force_remove_from_supplicant, reason);
}

NMRefString *
nm_supplicant_interface_get_name_owner(NMSupplicantInterface *self)
{
    g_return_val_if_fail(NM_IS_SUPPLICANT_INTERFACE(self), NULL);

    return NM_SUPPLICANT_INTERFACE_GET_PRIVATE(self)->name_owner;
}

NMRefString *
nm_supplicant_interface_get_object_path(NMSupplicantInterface *self)
{
    g_return_val_if_fail(NM_IS_SUPPLICANT_INTERFACE(self), NULL);

    return NM_SUPPLICANT_INTERFACE_GET_PRIVATE(self)->object_path;
}

const char *
nm_supplicant_interface_get_ifname(NMSupplicantInterface *self)
{
    g_return_val_if_fail(NM_IS_SUPPLICANT_INTERFACE(self), NULL);

    return NM_SUPPLICANT_INTERFACE_GET_PRIVATE(self)->ifname;
}

guint
nm_supplicant_interface_get_max_scan_ssids(NMSupplicantInterface *self)
{
    NMSupplicantInterfacePrivate *priv;

    g_return_val_if_fail(NM_IS_SUPPLICANT_INTERFACE(self), 0);

    priv = NM_SUPPLICANT_INTERFACE_GET_PRIVATE(self);
    return priv->prop_scan_active && priv->prop_scan_ssid ? priv->max_scan_ssids : 0u;
}

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

void
nm_supplicant_interface_p2p_start_find(NMSupplicantInterface *self, guint timeout)
{
    GVariantBuilder builder;

    g_return_if_fail(NM_IS_SUPPLICANT_INTERFACE(self));
    g_return_if_fail(timeout > 0 && timeout <= 600);

    g_variant_builder_init(&builder, G_VARIANT_TYPE_VARDICT);
    g_variant_builder_add(&builder, "{sv}", "Timeout", g_variant_new_int32(timeout));

    _dbus_connection_call_simple(self,
                                 NM_WPAS_DBUS_IFACE_INTERFACE_P2P_DEVICE,
                                 "Find",
                                 g_variant_new("(a{sv})", &builder),
                                 G_VARIANT_TYPE("()"),
                                 "p2p-find");
}

void
nm_supplicant_interface_p2p_stop_find(NMSupplicantInterface *self)
{
    g_return_if_fail(NM_IS_SUPPLICANT_INTERFACE(self));

    _dbus_connection_call_simple(self,
                                 NM_WPAS_DBUS_IFACE_INTERFACE_P2P_DEVICE,
                                 "StopFind",
                                 NULL,
                                 G_VARIANT_TYPE("()"),
                                 "p2p-stop-find");
}

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

void
nm_supplicant_interface_p2p_connect(NMSupplicantInterface *self,
                                    const char *           peer,
                                    const char *           wps_method,
                                    const char *           wps_pin)
{
    GVariantBuilder builder;

    g_return_if_fail(NM_IS_SUPPLICANT_INTERFACE(self));

    g_variant_builder_init(&builder, G_VARIANT_TYPE_VARDICT);

    g_variant_builder_add(&builder, "{sv}", "wps_method", g_variant_new_string(wps_method));
    if (wps_pin)
        g_variant_builder_add(&builder, "{sv}", "pin", g_variant_new_string(wps_pin));
    g_variant_builder_add(&builder, "{sv}", "peer", g_variant_new_object_path(peer));
    g_variant_builder_add(&builder, "{sv}", "join", g_variant_new_boolean(FALSE));
    g_variant_builder_add(&builder, "{sv}", "persistent", g_variant_new_boolean(FALSE));
    g_variant_builder_add(&builder, "{sv}", "go_intent", g_variant_new_int32(7));

    _dbus_connection_call_simple(self,
                                 NM_WPAS_DBUS_IFACE_INTERFACE_P2P_DEVICE,
                                 "Connect",
                                 g_variant_new("(a{sv})", &builder),
                                 G_VARIANT_TYPE("(s)"),
                                 "p2p-connect");
}

void
nm_supplicant_interface_p2p_cancel_connect(NMSupplicantInterface *self)
{
    g_return_if_fail(NM_IS_SUPPLICANT_INTERFACE(self));

    _dbus_connection_call_simple_full(self,
                                      NM_WPAS_DBUS_IFACE_INTERFACE_P2P_DEVICE,
                                      "Cancel",
                                      NULL,
                                      G_VARIANT_TYPE("()"),
                                      "p2p-cancel",
                                      TRUE);
}

void
nm_supplicant_interface_p2p_disconnect(NMSupplicantInterface *self)
{
    g_return_if_fail(NM_IS_SUPPLICANT_INTERFACE(self));

    _dbus_connection_call_simple(self,
                                 NM_WPAS_DBUS_IFACE_INTERFACE_P2P_DEVICE,
                                 "Disconnect",
                                 NULL,
                                 G_VARIANT_TYPE("()"),
                                 "p2p-disconnect");
}

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

static void
_properties_changed(NMSupplicantInterface *self,
                    const char *           interface_name,
                    GVariant *             properties,
                    gboolean               initial)
{
    NMSupplicantInterfacePrivate *priv = NM_SUPPLICANT_INTERFACE_GET_PRIVATE(self);
    gboolean                      is_main;

    nm_assert(!properties || g_variant_is_of_type(properties, G_VARIANT_TYPE("a{sv}")));

    if (initial)
        priv->starting_pending_count--;

    if ((initial || priv->is_ready_main) && nm_streq(interface_name, NM_WPAS_DBUS_IFACE_INTERFACE))
        is_main = TRUE;
    else if ((initial || priv->is_ready_p2p_device)
             && nm_streq(interface_name, NM_WPAS_DBUS_IFACE_INTERFACE_P2P_DEVICE)) {
        nm_assert(_get_capability(priv, NM_SUPPL_CAP_TYPE_P2P) == NM_TERNARY_TRUE);
        is_main = FALSE;
    } else
        return;

    g_object_freeze_notify(G_OBJECT(self));

    priv->starting_pending_count++;

    if (is_main) {
        priv->is_ready_main = TRUE;
        _properties_changed_main(self, properties);
    } else {
        priv->is_ready_p2p_device = TRUE;
        _properties_changed_p2p_device(self, properties);
    }

    priv->starting_pending_count--;
    _starting_check_ready(self);

    _notify_maybe_scanning(self);
    _notify_maybe_p2p_available(self);

    g_object_thaw_notify(G_OBJECT(self));
}

static void
_properties_changed_cb(GDBusConnection *connection,
                       const char *     sender_name,
                       const char *     object_path,
                       const char *     signal_interface_name,
                       const char *     signal_name,
                       GVariant *       parameters,
                       gpointer         user_data)
{
    NMSupplicantInterface *self = NM_SUPPLICANT_INTERFACE(user_data);
    const char *           interface_name;
    gs_unref_variant GVariant *changed_properties = NULL;

    if (!g_variant_is_of_type(parameters, G_VARIANT_TYPE("(sa{sv}as)")))
        return;

    g_variant_get(parameters, "(&s@a{sv}^a&s)", &interface_name, &changed_properties, NULL);
    _properties_changed(self, interface_name, changed_properties, FALSE);
}

static void
_bss_properties_changed_cb(GDBusConnection *connection,
                           const char *     sender_name,
                           const char *     object_path,
                           const char *     signal_interface_name,
                           const char *     signal_name,
                           GVariant *       parameters,
                           gpointer         user_data)
{
    NMSupplicantInterface *       self            = NM_SUPPLICANT_INTERFACE(user_data);
    NMSupplicantInterfacePrivate *priv            = NM_SUPPLICANT_INTERFACE_GET_PRIVATE(self);
    nm_auto_ref_string NMRefString *bss_path      = NULL;
    gs_unref_variant GVariant *changed_properties = NULL;
    NMSupplicantBssInfo *      bss_info;

    if (!g_variant_is_of_type(parameters, G_VARIANT_TYPE("(sa{sv}as)")))
        return;

    bss_path = nm_ref_string_new(object_path);

    bss_info = g_hash_table_lookup(priv->bss_idx, &bss_path);
    if (!bss_info)
        return;
    if (bss_info->_init_cancellable)
        return;

    g_variant_get(parameters, "(&s@a{sv}^a&s)", NULL, &changed_properties, NULL);
    _bss_info_properties_changed(self, bss_info, changed_properties, FALSE);
}

static void
_peer_properties_changed_cb(GDBusConnection *connection,
                            const char *     sender_name,
                            const char *     object_path,
                            const char *     signal_interface_name,
                            const char *     signal_name,
                            GVariant *       parameters,
                            gpointer         user_data)
{
    NMSupplicantInterface *       self            = NM_SUPPLICANT_INTERFACE(user_data);
    NMSupplicantInterfacePrivate *priv            = NM_SUPPLICANT_INTERFACE_GET_PRIVATE(self);
    nm_auto_ref_string NMRefString *peer_path     = NULL;
    gs_unref_variant GVariant *changed_properties = NULL;
    NMSupplicantPeerInfo *     peer_info;

    if (!g_variant_is_of_type(parameters, G_VARIANT_TYPE("(sa{sv}as)")))
        return;

    peer_path = nm_ref_string_new(object_path);

    peer_info = g_hash_table_lookup(priv->peer_idx, &peer_path);
    if (!peer_info)
        return;
    if (peer_info->_init_cancellable)
        return;

    g_variant_get(parameters, "(&s@a{sv}^a&s)", NULL, &changed_properties, NULL);
    _peer_info_properties_changed(self, peer_info, changed_properties, FALSE);
}

static void
_get_all_main_cb(GVariant *result, GError *error, gpointer user_data)
{
    gs_unref_variant GVariant *properties = NULL;

    if (nm_utils_error_is_cancelled(error))
        return;

    if (result)
        g_variant_get(result, "(@a{sv})", &properties);
    _properties_changed(user_data, NM_WPAS_DBUS_IFACE_INTERFACE, properties, TRUE);
}

static void
_get_all_p2p_device_cb(GVariant *result, GError *error, gpointer user_data)
{
    gs_unref_variant GVariant *properties = NULL;

    if (nm_utils_error_is_cancelled(error))
        return;

    if (result)
        g_variant_get(result, "(@a{sv})", &properties);
    _properties_changed(user_data, NM_WPAS_DBUS_IFACE_INTERFACE_P2P_DEVICE, properties, TRUE);
}

static void
_signal_handle(NMSupplicantInterface *self,
               const char *           signal_interface_name,
               const char *           signal_name,
               GVariant *             parameters)
{
    NMSupplicantInterfacePrivate *priv = NM_SUPPLICANT_INTERFACE_GET_PRIVATE(self);
    const char *                  path;

    if (nm_streq(signal_interface_name, NM_WPAS_DBUS_IFACE_INTERFACE)) {
        if (!priv->is_ready_main)
            return;

        if (nm_streq(signal_name, "BSSAdded")) {
            if (!g_variant_is_of_type(parameters, G_VARIANT_TYPE("(oa{sv})")))
                return;

            g_variant_get(parameters, "(&oa{sv})", &path, NULL);
            _bss_info_add(self, path);
            return;
        }

        if (nm_streq(signal_name, "BSSRemoved")) {
            nm_auto_ref_string NMRefString *bss_path = NULL;

            if (!g_variant_is_of_type(parameters, G_VARIANT_TYPE("(o)")))
                return;

            g_variant_get(parameters, "(&o)", &path);
            bss_path = nm_ref_string_new(path);
            _bss_info_remove(self, &bss_path);
            return;
        }

        if (nm_streq(signal_name, "EAP")) {
            NMSupplicantAuthState auth_state = NM_SUPPLICANT_AUTH_STATE_UNKNOWN;
            const char *          status;
            const char *          parameter;

            if (g_variant_is_of_type(parameters, G_VARIANT_TYPE("(ss)")))
                return;

            g_variant_get(parameters, "(&s&s)", &status, &parameter);

            if (nm_streq(status, "started"))
                auth_state = NM_SUPPLICANT_AUTH_STATE_STARTED;
            else if (nm_streq(status, "completion")) {
                if (nm_streq(parameter, "success"))
                    auth_state = NM_SUPPLICANT_AUTH_STATE_SUCCESS;
                else if (nm_streq(parameter, "failure"))
                    auth_state = NM_SUPPLICANT_AUTH_STATE_FAILURE;
            }

            /* the state eventually reaches one of started, success or failure
             * so ignore any other intermediate (unknown) state change. */
            if (auth_state != NM_SUPPLICANT_AUTH_STATE_UNKNOWN && auth_state != priv->auth_state) {
                priv->auth_state = auth_state;
                _notify(self, PROP_AUTH_STATE);
            }
            return;
        }

        return;
    }

    if (nm_streq(signal_interface_name, NM_WPAS_DBUS_IFACE_INTERFACE_P2P_DEVICE)) {
        if (!priv->is_ready_p2p_device)
            return;

        if (nm_streq(signal_name, "DeviceFound")) {
            if (g_variant_is_of_type(parameters, G_VARIANT_TYPE("(o)"))) {
                g_variant_get(parameters, "(&o)", &path);
                _peer_info_add(self, path);
            }
            return;
        }

        if (nm_streq(signal_name, "DeviceLost")) {
            if (g_variant_is_of_type(parameters, G_VARIANT_TYPE("(o)"))) {
                nm_auto_ref_string NMRefString *peer_path = NULL;

                g_variant_get(parameters, "(&o)", &path);
                peer_path = nm_ref_string_new(path);
                _peer_info_remove(self, &peer_path);
            }
            return;
        }

        if (nm_streq(signal_name, "GroupStarted")) {
            if (g_variant_is_of_type(parameters, G_VARIANT_TYPE("(a{sv})"))) {
                gs_unref_variant GVariant *args              = NULL;
                gs_unref_object NMSupplicantInterface *iface = NULL;
                const char *                           group_path;
                const char *                           iface_path;

                g_variant_get(parameters, "(@a{sv})", &args);
                if (!g_variant_lookup(args, "group_object", "&o", &group_path))
                    return;
                if (!g_variant_lookup(args, "interface_object", "&o", &iface_path))
                    return;

                if (nm_streq(iface_path, priv->object_path->str)) {
                    _LOGW("P2P: GroupStarted on existing interface");
                    iface = g_object_ref(self);
                } else {
                    iface =
                        nm_supplicant_manager_create_interface_from_path(priv->supplicant_manager,
                                                                         iface_path);
                    if (iface == NULL) {
                        _LOGW("P2P: Group interface already exists in GroupStarted handler, "
                              "aborting further processing.");
                        return;
                    }
                }

                /* Signal existence of the (new) interface. */
                g_signal_emit(self, signals[GROUP_STARTED], 0, iface);
            }
            return;
        }

        if (nm_streq(signal_name, "GroupFinished")) {
            if (g_variant_is_of_type(parameters, G_VARIANT_TYPE("(a{sv})"))) {
                gs_unref_variant GVariant *args = NULL;
                const char *               iface_path;

                g_variant_get(parameters, "(@a{sv})", &args);

                /* TODO: Group finished is called on the management interface!
                 *       This means the signal consumer will currently need to assume which
                 *       interface is finishing or it needs to match the object paths.
                 */
                if (!g_variant_lookup(args, "interface_object", "&o", &iface_path))
                    return;

                _LOGD("P2P: GroupFinished signal on interface %s for interface %s",
                      priv->object_path->str,
                      iface_path);

                /* Signal group finish interface (on management interface). */
                g_signal_emit(self, signals[GROUP_FINISHED], 0, iface_path);
            }
            return;
        }

        return;
    }
}

static void
_signal_cb(GDBusConnection *connection,
           const char *     sender_name,
           const char *     object_path,
           const char *     signal_interface_name,
           const char *     signal_name,
           GVariant *       parameters,
           gpointer         user_data)
{
    NMSupplicantInterface *       self = user_data;
    NMSupplicantInterfacePrivate *priv = NM_SUPPLICANT_INTERFACE_GET_PRIVATE(self);

    priv->starting_pending_count++;
    _signal_handle(self, signal_interface_name, signal_name, parameters);
    priv->starting_pending_count--;
    _starting_check_ready(self);

    _notify_maybe_scanning(self);
}

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

gboolean
nm_supplicant_interface_get_p2p_available(NMSupplicantInterface *self)
{
    return NM_SUPPLICANT_INTERFACE_GET_PRIVATE(self)->p2p_capable_cached;
}

gboolean
nm_supplicant_interface_get_p2p_group_joined(NMSupplicantInterface *self)
{
    return NM_SUPPLICANT_INTERFACE_GET_PRIVATE(self)->p2p_group_joined_cached;
}

const char *
nm_supplicant_interface_get_p2p_group_path(NMSupplicantInterface *self)
{
    return nm_ref_string_get_str(NM_SUPPLICANT_INTERFACE_GET_PRIVATE(self)->p2p_group_path);
}

gboolean
nm_supplicant_interface_get_p2p_group_owner(NMSupplicantInterface *self)
{
    return NM_SUPPLICANT_INTERFACE_GET_PRIVATE(self)->p2p_group_owner_cached;
}

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

static void
get_property(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec)
{
    NMSupplicantInterface *       self = NM_SUPPLICANT_INTERFACE(object);
    NMSupplicantInterfacePrivate *priv = NM_SUPPLICANT_INTERFACE_GET_PRIVATE(self);

    switch (prop_id) {
    case PROP_SCANNING:
        g_value_set_boolean(value, nm_supplicant_interface_get_scanning(self));
        break;
    case PROP_CURRENT_BSS:
        g_value_set_string(value,
                           nm_ref_string_get_str(nm_supplicant_interface_get_current_bss(self)));
        break;
    case PROP_P2P_GROUP_JOINED:
        g_value_set_boolean(value, nm_supplicant_interface_get_p2p_group_joined(self));
        break;
    case PROP_P2P_GROUP_PATH:
        g_value_set_string(value, nm_supplicant_interface_get_p2p_group_path(self));
        break;
    case PROP_P2P_GROUP_OWNER:
        g_value_set_boolean(value, nm_supplicant_interface_get_p2p_group_owner(self));
        break;
    case PROP_P2P_AVAILABLE:
        g_value_set_boolean(value, nm_supplicant_interface_get_p2p_available(self));
        break;
    case PROP_AUTH_STATE:
        g_value_set_uint(value, priv->auth_state);
        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)
{
    NMSupplicantInterfacePrivate *priv = NM_SUPPLICANT_INTERFACE_GET_PRIVATE(object);

    switch (prop_id) {
    case PROP_SUPPLICANT_MANAGER:
        /* construct-only */
        priv->supplicant_manager = g_object_ref(g_value_get_pointer(value));
        nm_assert(NM_IS_SUPPLICANT_MANAGER(priv->supplicant_manager));

        priv->dbus_connection =
            g_object_ref(nm_supplicant_manager_get_dbus_connection(priv->supplicant_manager));
        nm_assert(G_IS_DBUS_CONNECTION(priv->dbus_connection));

        priv->name_owner =
            nm_ref_string_ref(nm_supplicant_manager_get_dbus_name_owner(priv->supplicant_manager));
        nm_assert(NM_IS_REF_STRING(priv->name_owner));

        priv->global_capabilities =
            nm_supplicant_manager_get_global_capabilities(priv->supplicant_manager);
        break;
    case PROP_DBUS_OBJECT_PATH:
        /* construct-only */
        priv->object_path = nm_ref_string_ref(g_value_get_pointer(value));
        nm_assert(NM_IS_REF_STRING(priv->object_path));
        break;
    case PROP_IFINDEX:
        /* construct-only */
        priv->ifindex = g_value_get_int(value);
        break;
    case PROP_DRIVER:
        /* construct-only */
        priv->requested_driver = g_value_get_uint(value);
        break;
    default:
        G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
        break;
    }
}

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

static void
nm_supplicant_interface_init(NMSupplicantInterface *self)
{
    NMSupplicantInterfacePrivate *priv;

    priv = G_TYPE_INSTANCE_GET_PRIVATE(self,
                                       NM_TYPE_SUPPLICANT_INTERFACE,
                                       NMSupplicantInterfacePrivate);

    self->_priv = priv;

    nm_assert(priv->global_capabilities == NM_SUPPL_CAP_MASK_NONE);
    nm_assert(priv->iface_capabilities == NM_SUPPL_CAP_MASK_NONE);

    priv->state          = NM_SUPPLICANT_INTERFACE_STATE_STARTING;
    priv->supp_state     = NM_SUPPLICANT_INTERFACE_STATE_INVALID;
    priv->last_scan_msec = -1;

    c_list_init(&self->supp_lst);

    G_STATIC_ASSERT_EXPR(G_STRUCT_OFFSET(NMSupplicantBssInfo, bss_path) == 0);
    priv->bss_idx = g_hash_table_new(nm_pdirect_hash, nm_pdirect_equal);

    c_list_init(&priv->bss_lst_head);
    c_list_init(&priv->bss_initializing_lst_head);

    G_STATIC_ASSERT_EXPR(G_STRUCT_OFFSET(NMSupplicantPeerInfo, peer_path) == 0);
    priv->peer_idx = g_hash_table_new(nm_pdirect_hash, nm_pdirect_equal);

    c_list_init(&priv->peer_lst_head);
    c_list_init(&priv->peer_initializing_lst_head);

    priv->main_cancellable = g_cancellable_new();
}

static void
constructed(GObject *object)
{
    NMSupplicantInterface *       self = NM_SUPPLICANT_INTERFACE(object);
    NMSupplicantInterfacePrivate *priv = NM_SUPPLICANT_INTERFACE_GET_PRIVATE(self);

    G_OBJECT_CLASS(nm_supplicant_interface_parent_class)->constructed(object);

    _LOGD("new supplicant interface %s on %s", priv->object_path->str, priv->name_owner->str);

    priv->properties_changed_id =
        nm_dbus_connection_signal_subscribe_properties_changed(priv->dbus_connection,
                                                               priv->name_owner->str,
                                                               priv->object_path->str,
                                                               NULL,
                                                               _properties_changed_cb,
                                                               self,
                                                               NULL);

    priv->bss_properties_changed_id =
        nm_dbus_connection_signal_subscribe_properties_changed(priv->dbus_connection,
                                                               priv->name_owner->str,
                                                               NULL,
                                                               NM_WPAS_DBUS_IFACE_BSS,
                                                               _bss_properties_changed_cb,
                                                               self,
                                                               NULL);

    priv->signal_id = g_dbus_connection_signal_subscribe(priv->dbus_connection,
                                                         priv->name_owner->str,
                                                         NULL,
                                                         NULL,
                                                         priv->object_path->str,
                                                         NULL,
                                                         G_DBUS_SIGNAL_FLAGS_NONE,
                                                         _signal_cb,
                                                         self,
                                                         NULL);

    /* Scan result aging parameters */
    nm_dbus_connection_call_set(priv->dbus_connection,
                                priv->name_owner->str,
                                priv->object_path->str,
                                NM_WPAS_DBUS_IFACE_INTERFACE,
                                "BSSExpireAge",
                                g_variant_new_uint32(250),
                                DBUS_TIMEOUT_MSEC,
                                NULL,
                                NULL,
                                NULL);
    nm_dbus_connection_call_set(priv->dbus_connection,
                                priv->name_owner->str,
                                priv->object_path->str,
                                NM_WPAS_DBUS_IFACE_INTERFACE,
                                "BSSExpireCount",
                                g_variant_new_uint32(2),
                                DBUS_TIMEOUT_MSEC,
                                NULL,
                                NULL,
                                NULL);

    if (_get_capability(priv, NM_SUPPL_CAP_TYPE_PMF) == NM_TERNARY_TRUE) {
        /* Initialize global PMF setting to 'optional' */
        nm_dbus_connection_call_set(priv->dbus_connection,
                                    priv->name_owner->str,
                                    priv->object_path->str,
                                    NM_WPAS_DBUS_IFACE_INTERFACE,
                                    "Pmf",
                                    g_variant_new_string("1"),
                                    DBUS_TIMEOUT_MSEC,
                                    NULL,
                                    NULL,
                                    NULL);
    }

    if (_get_capability(priv, NM_SUPPL_CAP_TYPE_AP) == NM_TERNARY_DEFAULT) {
        /* If the global supplicant capabilities property is not present, we can
         * fall back to checking whether the ProbeRequest method is supported.  If
         * neither of these works we have no way of determining if AP mode is
         * supported or not.  hostap 1.0 and earlier don't support either of these.
         */
        priv->starting_pending_count++;
        _dbus_connection_call(self,
                              DBUS_INTERFACE_INTROSPECTABLE,
                              "Introspect",
                              NULL,
                              G_VARIANT_TYPE("(s)"),
                              G_DBUS_CALL_FLAGS_NONE,
                              5000,
                              priv->main_cancellable,
                              iface_introspect_cb,
                              self);
    }

    priv->starting_pending_count++;
    nm_dbus_connection_call_get_all(priv->dbus_connection,
                                    priv->name_owner->str,
                                    priv->object_path->str,
                                    NM_WPAS_DBUS_IFACE_INTERFACE,
                                    5000,
                                    priv->main_cancellable,
                                    _get_all_main_cb,
                                    self);

    if (_get_capability(priv, NM_SUPPL_CAP_TYPE_P2P) == NM_TERNARY_TRUE) {
        priv->peer_properties_changed_id =
            nm_dbus_connection_signal_subscribe_properties_changed(priv->dbus_connection,
                                                                   priv->name_owner->str,
                                                                   NULL,
                                                                   NM_WPAS_DBUS_IFACE_PEER,
                                                                   _peer_properties_changed_cb,
                                                                   self,
                                                                   NULL);

        priv->starting_pending_count++;
        nm_dbus_connection_call_get_all(priv->dbus_connection,
                                        priv->name_owner->str,
                                        priv->object_path->str,
                                        NM_WPAS_DBUS_IFACE_INTERFACE_P2P_DEVICE,
                                        5000,
                                        priv->main_cancellable,
                                        _get_all_p2p_device_cb,
                                        self);
    }
}

NMSupplicantInterface *
nm_supplicant_interface_new(NMSupplicantManager *supplicant_manager,
                            NMRefString *        object_path,
                            int                  ifindex,
                            NMSupplicantDriver   driver)
{
    nm_assert(NM_IS_SUPPLICANT_MANAGER(supplicant_manager));

    return g_object_new(NM_TYPE_SUPPLICANT_INTERFACE,
                        NM_SUPPLICANT_INTERFACE_SUPPLICANT_MANAGER,
                        supplicant_manager,
                        NM_SUPPLICANT_INTERFACE_DBUS_OBJECT_PATH,
                        object_path,
                        NM_SUPPLICANT_INTERFACE_IFINDEX,
                        ifindex,
                        NM_SUPPLICANT_INTERFACE_DRIVER,
                        (guint) driver,
                        NULL);
}

static void
dispose(GObject *object)
{
    NMSupplicantInterface *       self = NM_SUPPLICANT_INTERFACE(object);
    NMSupplicantInterfacePrivate *priv = NM_SUPPLICANT_INTERFACE_GET_PRIVATE(self);

    if (priv->state != NM_SUPPLICANT_INTERFACE_STATE_DOWN)
        set_state_down(self, TRUE, "NMSupplicantInterface is disposing");

    nm_assert(c_list_is_empty(&self->supp_lst));

    if (priv->wps_data) {
        /* we shut down, but an asynchronous Cancel request is pending.
         * We don't want to cancel it, so mark wps-data that @self is gone.
         * This way, _wps_handle_cancel_cb() knows it must no longer touch
         * @self */
        priv->wps_data->self = NULL;
        priv->wps_data       = NULL;
    }

    nm_assert(!priv->assoc_data);

    nm_clear_pointer(&priv->bss_idx, g_hash_table_destroy);
    nm_clear_pointer(&priv->peer_idx, g_hash_table_destroy);

    nm_clear_pointer(&priv->current_bss, nm_ref_string_unref);

    G_OBJECT_CLASS(nm_supplicant_interface_parent_class)->dispose(object);

    nm_clear_pointer(&priv->object_path, nm_ref_string_unref);
    nm_clear_pointer(&priv->name_owner, nm_ref_string_unref);
    g_clear_object(&priv->supplicant_manager);
    g_clear_object(&priv->dbus_connection);
    nm_clear_g_free(&priv->ifname);
    nm_clear_g_free(&priv->driver);
    nm_assert(!priv->net_path);
}

static void
nm_supplicant_interface_class_init(NMSupplicantInterfaceClass *klass)
{
    GObjectClass *object_class = G_OBJECT_CLASS(klass);

    g_type_class_add_private(object_class, sizeof(NMSupplicantInterfacePrivate));

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

    obj_properties[PROP_SUPPLICANT_MANAGER] =
        g_param_spec_pointer(NM_SUPPLICANT_INTERFACE_SUPPLICANT_MANAGER,
                             "",
                             "",
                             G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
    obj_properties[PROP_DBUS_OBJECT_PATH] =
        g_param_spec_pointer(NM_SUPPLICANT_INTERFACE_DBUS_OBJECT_PATH,
                             "",
                             "",
                             G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
    obj_properties[PROP_IFINDEX] =
        g_param_spec_int(NM_SUPPLICANT_INTERFACE_IFINDEX,
                         "",
                         "",
                         0,
                         G_MAXINT,
                         0,
                         G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
    obj_properties[PROP_DRIVER] =
        g_param_spec_uint(NM_SUPPLICANT_INTERFACE_DRIVER,
                          "",
                          "",
                          0,
                          G_MAXUINT,
                          NM_SUPPLICANT_DRIVER_WIRELESS,
                          G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);

    obj_properties[PROP_SCANNING] = g_param_spec_boolean(NM_SUPPLICANT_INTERFACE_SCANNING,
                                                         "",
                                                         "",
                                                         FALSE,
                                                         G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
    obj_properties[PROP_CURRENT_BSS] =
        g_param_spec_string(NM_SUPPLICANT_INTERFACE_CURRENT_BSS,
                            "",
                            "",
                            NULL,
                            G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
    obj_properties[PROP_P2P_GROUP_JOINED] =
        g_param_spec_boolean(NM_SUPPLICANT_INTERFACE_P2P_GROUP_JOINED,
                             "",
                             "",
                             FALSE,
                             G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
    obj_properties[PROP_P2P_GROUP_PATH] =
        g_param_spec_string(NM_SUPPLICANT_INTERFACE_P2P_GROUP_PATH,
                            "",
                            "",
                            NULL,
                            G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
    obj_properties[PROP_P2P_GROUP_OWNER] =
        g_param_spec_boolean(NM_SUPPLICANT_INTERFACE_P2P_GROUP_OWNER,
                             "",
                             "",
                             FALSE,
                             G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
    obj_properties[PROP_P2P_AVAILABLE] =
        g_param_spec_boolean(NM_SUPPLICANT_INTERFACE_P2P_AVAILABLE,
                             "",
                             "",
                             FALSE,
                             G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
    obj_properties[PROP_AUTH_STATE] = g_param_spec_uint(NM_SUPPLICANT_INTERFACE_AUTH_STATE,
                                                        "",
                                                        "",
                                                        NM_SUPPLICANT_AUTH_STATE_UNKNOWN,
                                                        _NM_SUPPLICANT_AUTH_STATE_NUM - 1,
                                                        NM_SUPPLICANT_AUTH_STATE_UNKNOWN,
                                                        G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);

    g_object_class_install_properties(object_class, _PROPERTY_ENUMS_LAST, obj_properties);

    signals[STATE] = g_signal_new(NM_SUPPLICANT_INTERFACE_STATE,
                                  G_OBJECT_CLASS_TYPE(object_class),
                                  G_SIGNAL_RUN_LAST,
                                  0,
                                  NULL,
                                  NULL,
                                  NULL,
                                  G_TYPE_NONE,
                                  3,
                                  G_TYPE_INT,
                                  G_TYPE_INT,
                                  G_TYPE_INT);

    signals[BSS_CHANGED] = g_signal_new(NM_SUPPLICANT_INTERFACE_BSS_CHANGED,
                                        G_OBJECT_CLASS_TYPE(object_class),
                                        G_SIGNAL_RUN_LAST,
                                        0,
                                        NULL,
                                        NULL,
                                        NULL,
                                        G_TYPE_NONE,
                                        2,
                                        G_TYPE_POINTER,
                                        G_TYPE_BOOLEAN);

    signals[PEER_CHANGED] = g_signal_new(NM_SUPPLICANT_INTERFACE_PEER_CHANGED,
                                         G_OBJECT_CLASS_TYPE(object_class),
                                         G_SIGNAL_RUN_LAST,
                                         0,
                                         NULL,
                                         NULL,
                                         NULL,
                                         G_TYPE_NONE,
                                         2,
                                         G_TYPE_POINTER,
                                         G_TYPE_BOOLEAN);

    signals[WPS_CREDENTIALS] = g_signal_new(NM_SUPPLICANT_INTERFACE_WPS_CREDENTIALS,
                                            G_OBJECT_CLASS_TYPE(object_class),
                                            G_SIGNAL_RUN_LAST,
                                            0,
                                            NULL,
                                            NULL,
                                            NULL,
                                            G_TYPE_NONE,
                                            1,
                                            G_TYPE_VARIANT);

    signals[GROUP_STARTED] = g_signal_new(NM_SUPPLICANT_INTERFACE_GROUP_STARTED,
                                          G_OBJECT_CLASS_TYPE(object_class),
                                          G_SIGNAL_RUN_LAST,
                                          0,
                                          NULL,
                                          NULL,
                                          NULL,
                                          G_TYPE_NONE,
                                          1,
                                          NM_TYPE_SUPPLICANT_INTERFACE);

    signals[GROUP_FINISHED] = g_signal_new(NM_SUPPLICANT_INTERFACE_GROUP_FINISHED,
                                           G_OBJECT_CLASS_TYPE(object_class),
                                           G_SIGNAL_RUN_LAST,
                                           0,
                                           NULL,
                                           NULL,
                                           NULL,
                                           G_TYPE_NONE,
                                           1,
                                           G_TYPE_STRING);
}
