about summary refs log tree commit diff
path: root/src/core/devices/wifi
diff options
context:
space:
mode:
authorMichael Biebl <biebl@debian.org>2023-02-10 11:51:50 +0100
committerMichael Biebl <biebl@debian.org>2023-02-10 11:51:50 +0100
commit58dddb802071d967ff5731f3333239ead78248e8 (patch)
treedef9e578e4786f8cbc56ce1b6cabf7670e00ae81 /src/core/devices/wifi
parent2c1b7a7ea3c332b4d690c0595ef76c81de1f0cc8 (diff)
parent1372848511cb896b80b51ed1a3e9606bd9816631 (diff)
Update upstream source from tag 'upstream/1.42.0'
Update to upstream version '1.42.0'
with Debian dir 2b9fa2718b2bd34fa58008e335a0d81b87ad57cf
Diffstat (limited to 'src/core/devices/wifi')
-rw-r--r--src/core/devices/wifi/nm-device-iwd-p2p.h2
-rw-r--r--src/core/devices/wifi/nm-device-iwd.c466
-rw-r--r--src/core/devices/wifi/nm-device-iwd.h4
-rw-r--r--src/core/devices/wifi/nm-device-olpc-mesh.h2
-rw-r--r--src/core/devices/wifi/nm-device-wifi-p2p.c1
-rw-r--r--src/core/devices/wifi/nm-device-wifi-p2p.h2
-rw-r--r--src/core/devices/wifi/nm-device-wifi.c49
-rw-r--r--src/core/devices/wifi/nm-device-wifi.h3
-rw-r--r--src/core/devices/wifi/nm-iwd-manager.c261
-rw-r--r--src/core/devices/wifi/nm-iwd-manager.h7
-rw-r--r--src/core/devices/wifi/nm-wifi-ap.h2
-rw-r--r--src/core/devices/wifi/nm-wifi-factory.c2
-rw-r--r--src/core/devices/wifi/nm-wifi-p2p-peer.h2
-rw-r--r--src/core/devices/wifi/nm-wifi-utils.c175
14 files changed, 790 insertions, 188 deletions
diff --git a/src/core/devices/wifi/nm-device-iwd-p2p.h b/src/core/devices/wifi/nm-device-iwd-p2p.h
index 9ce353c0..98cf3762 100644
--- a/src/core/devices/wifi/nm-device-iwd-p2p.h
+++ b/src/core/devices/wifi/nm-device-iwd-p2p.h
@@ -11,7 +11,7 @@
 
 #define NM_TYPE_DEVICE_IWD_P2P (nm_device_iwd_p2p_get_type())
 #define NM_DEVICE_IWD_P2P(obj) \
-    (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DEVICE_IWD_P2P, NMDeviceIwdP2P))
+    (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DEVICE_IWD_P2P, NMDeviceIwdP2P))
 #define NM_DEVICE_IWD_P2P_CLASS(klass) \
     (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_DEVICE_IWD_P2P, NMDeviceIwdP2PClass))
 #define NM_IS_DEVICE_IWD_P2P(obj)         (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_DEVICE_IWD_P2P))
diff --git a/src/core/devices/wifi/nm-device-iwd.c b/src/core/devices/wifi/nm-device-iwd.c
index ab37cbec..e03227cd 100644
--- a/src/core/devices/wifi/nm-device-iwd.c
+++ b/src/core/devices/wifi/nm-device-iwd.c
@@ -8,6 +8,7 @@
 #include "nm-device-iwd.h"
 
 #include <linux/if_ether.h>
+#include <linux/rtnetlink.h>
 
 #include "devices/nm-device-private.h"
 #include "devices/nm-device.h"
@@ -32,6 +33,7 @@
 #include "supplicant/nm-supplicant-types.h"
 #include "nm-auth-utils.h"
 #include "nm-manager.h"
+#include "nm-l3-config-data.h"
 
 #define _NMLOG_DEVICE_TYPE NMDeviceIwd
 #include "devices/nm-device-logging.h"
@@ -70,6 +72,7 @@ typedef struct {
     bool                          secrets_failed : 1;
     bool                          networks_requested : 1;
     bool                          networks_changed : 1;
+    bool                          assuming : 1;
     gint64                        last_scan;
     uint32_t                      ap_id;
     guint32                       rate;
@@ -77,6 +80,16 @@ typedef struct {
     GDBusMethodInvocation        *pending_agent_request;
     NMActiveConnection           *assumed_ac;
     guint                         assumed_ac_timeout;
+
+    union {
+        struct {
+            const NML3ConfigData *pending_l3cd_6;
+            const NML3ConfigData *pending_l3cd_4;
+        };
+        const NML3ConfigData *pending_l3cd_x[2];
+    };
+
+    NMIwdManager *manager;
 } NMDeviceIwdPrivate;
 
 struct _NMDeviceIwd {
@@ -289,6 +302,7 @@ insert_ap_from_network(NMDeviceIwd *self,
                        gint64       last_seen_msec,
                        int16_t      signal)
 {
+    NMDeviceIwdPrivate             *priv          = NM_DEVICE_IWD_GET_PRIVATE(self);
     gs_unref_object GDBusProxy     *network_proxy = NULL;
     nm_auto_ref_string NMRefString *bss_path      = nm_ref_string_new(path);
     NMWifiAP                       *ap;
@@ -299,7 +313,7 @@ insert_ap_from_network(NMDeviceIwd *self,
     }
 
     network_proxy =
-        nm_iwd_manager_get_dbus_interface(nm_iwd_manager_get(), path, NM_IWD_NETWORK_INTERFACE);
+        nm_iwd_manager_get_dbus_interface(priv->manager, path, NM_IWD_NETWORK_INTERFACE);
 
     ap = ap_from_network(self, network_proxy, bss_path, last_seen_msec, signal);
     if (!ap)
@@ -525,6 +539,9 @@ cleanup_association_attempt(NMDeviceIwd *self, gboolean disconnect)
 
     if (disconnect && priv->dbus_station_proxy)
         send_disconnect(self);
+
+    nm_clear_l3cd(&priv->pending_l3cd_6);
+    nm_clear_l3cd(&priv->pending_l3cd_4);
 }
 
 static void
@@ -581,6 +598,10 @@ deactivate(NMDevice *device)
     if (!priv->dbus_obj)
         return;
 
+    /* Don't cause IWD to break the connection being assumed */
+    if (priv->assuming)
+        return;
+
     if (priv->dbus_station_proxy) {
         gs_unref_variant GVariant *value =
             g_dbus_proxy_get_cached_property(priv->dbus_station_proxy, "State");
@@ -673,7 +694,7 @@ deactivate_async(NMDevice                  *device,
 }
 
 static gboolean
-is_connection_known_network(NMConnection *connection)
+is_connection_known_network(NMIwdManager *manager, NMConnection *connection)
 {
     NMIwdNetworkSecurity security;
     gs_free char        *ssid = NULL;
@@ -681,17 +702,17 @@ is_connection_known_network(NMConnection *connection)
     if (!nm_wifi_connection_get_iwd_ssid_and_security(connection, &ssid, &security))
         return FALSE;
 
-    return nm_iwd_manager_is_known_network(nm_iwd_manager_get(), ssid, security);
+    return nm_iwd_manager_is_known_network(manager, ssid, security);
 }
 
 static gboolean
-is_ap_known_network(NMWifiAP *ap)
+is_ap_known_network(NMIwdManager *manager, NMWifiAP *ap)
 {
     gs_unref_object GDBusProxy *network_proxy = NULL;
     gs_unref_variant GVariant  *known_network = NULL;
 
     network_proxy =
-        nm_iwd_manager_get_dbus_interface(nm_iwd_manager_get(),
+        nm_iwd_manager_get_dbus_interface(manager,
                                           nm_ref_string_get_str(nm_wifi_ap_get_supplicant_path(ap)),
                                           NM_IWD_NETWORK_INTERFACE);
     if (!network_proxy)
@@ -794,7 +815,8 @@ check_connection_compatible(NMDevice *device, NMConnection *connection, GError *
          * thus are Known Networks.
          */
         if (security == NM_IWD_NETWORK_SECURITY_8021X) {
-            if (!is_connection_known_network(connection)) {
+            if (!is_connection_known_network(priv->manager, connection)
+                && !nm_iwd_manager_is_recently_mirrored(priv->manager, ssid)) {
                 nm_utils_error_set_literal(error,
                                            NM_UTILS_ERROR_CONNECTION_AVAILABLE_INCOMPATIBLE,
                                            "802.1x connections must have IWD provisioning files");
@@ -927,7 +949,9 @@ check_connection_available(NMDevice                      *device,
      */
     if (nm_wifi_connection_get_iwd_ssid_and_security(connection, NULL, &security)
         && security == NM_IWD_NETWORK_SECURITY_8021X) {
-        if (!is_ap_known_network(ap)) {
+        if (!is_ap_known_network(priv->manager, ap)
+            && !nm_iwd_manager_is_recently_mirrored(priv->manager,
+                                                    nm_setting_wireless_get_ssid(s_wifi))) {
             nm_utils_error_set_literal(
                 error,
                 NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY,
@@ -2044,7 +2068,7 @@ assume_connection(NMDeviceIwd *self, NMWifiAP *ap)
      * becomes "managed" only when ACTIVATED but for IWD it's really
      * managed when IP_CONFIG starts.
      */
-    sett_conn = nm_iwd_manager_get_ap_mirror_connection(nm_iwd_manager_get(), ap);
+    sett_conn = nm_iwd_manager_get_ap_mirror_connection(priv->manager, ap);
     if (!sett_conn)
         goto error;
 
@@ -2217,7 +2241,8 @@ act_stage1_prepare(NMDevice *device, NMDeviceStateReason *out_failure_reason)
      * for a first-time connection to a hidden network.  If a hidden network is
      * a Known Network it should still have been in the AP list.
      */
-    if (!nm_setting_wireless_get_hidden(s_wireless) || is_connection_known_network(connection))
+    if (!nm_setting_wireless_get_hidden(s_wireless)
+        || is_connection_known_network(priv->manager, connection))
         return NM_ACT_STAGE_RETURN_FAILURE;
 
 add_new:
@@ -2270,6 +2295,18 @@ act_stage2_config(NMDevice *device, NMDeviceStateReason *out_failure_reason)
             goto out_fail;
         }
 
+        /* With priv->iwd_autoconnect we have to let IWD handle retries for
+         * infrastructure networks.  IWD will not necessarily retry the same
+         * network after a failure but it will likely go into an autoconnect
+         * mode and we don't want to try to override the logic.  We don't need
+         * to reset the retry count so we set no timeout.
+         */
+        if (priv->iwd_autoconnect) {
+            NMSettingsConnection *sett_conn = nm_act_request_get_settings_connection(req);
+
+            nm_settings_connection_autoconnect_retries_set(sett_conn, 0);
+        }
+
         /* With priv->iwd_autoconnect, if we're assuming a connection because
          * of a state change to "connecting", signal stage 2 is still running.
          * If "connected" or "roaming", we can go right to the IP_CONFIG state
@@ -2310,7 +2347,9 @@ act_stage2_config(NMDevice *device, NMDeviceStateReason *out_failure_reason)
          * fail, for other combinations we will let the Connect call fail
          * or ask us for any missing secrets through the Agent.
          */
-        if (nm_connection_get_setting_802_1x(connection) && !is_ap_known_network(ap)) {
+        if (nm_connection_get_setting_802_1x(connection) && !is_ap_known_network(priv->manager, ap)
+            && !nm_iwd_manager_is_recently_mirrored(priv->manager,
+                                                    nm_setting_wireless_get_ssid(s_wireless))) {
             _LOGI(LOGD_DEVICE | LOGD_WIFI,
                   "Activation: (wifi) access point '%s' has 802.1x security but is not configured "
                   "in IWD.",
@@ -2351,7 +2390,7 @@ act_stage2_config(NMDevice *device, NMDeviceStateReason *out_failure_reason)
         }
 
         network_proxy = nm_iwd_manager_get_dbus_interface(
-            nm_iwd_manager_get(),
+            priv->manager,
             nm_ref_string_get_str(nm_wifi_ap_get_supplicant_path(ap)),
             NM_IWD_NETWORK_INTERFACE);
         if (!network_proxy) {
@@ -2421,6 +2460,38 @@ out_fail:
     return NM_ACT_STAGE_RETURN_FAILURE;
 }
 
+static void
+act_stage3_ip_config(NMDevice *device, int addr_family)
+{
+    NMDeviceIwd        *self    = NM_DEVICE_IWD(device);
+    NMDeviceIwdPrivate *priv    = NM_DEVICE_IWD_GET_PRIVATE(self);
+    const int           IS_IPv4 = NM_IS_IPv4(addr_family);
+
+    if (!nm_iwd_manager_get_netconfig_enabled(nm_iwd_manager_get())) {
+        NMDeviceClass *device_class = NM_DEVICE_CLASS(nm_device_iwd_parent_class);
+
+        if (device_class->act_stage3_ip_config)
+            device_class->act_stage3_ip_config(device, addr_family);
+
+        return;
+    }
+
+    if (!priv->pending_l3cd_x[IS_IPv4])
+        return;
+
+    nm_device_devip_set_state(device,
+                              addr_family,
+                              NM_DEVICE_IP_STATE_READY,
+                              priv->pending_l3cd_x[IS_IPv4]);
+    nm_clear_l3cd(&priv->pending_l3cd_x[IS_IPv4]);
+}
+
+static gboolean
+ready_for_ip_config(NMDevice *device, gboolean is_manual)
+{
+    return !nm_iwd_manager_get_netconfig_enabled(nm_iwd_manager_get());
+}
+
 static guint32
 get_configured_mtu(NMDevice *device, NMDeviceMtuSource *out_source, gboolean *out_force)
 {
@@ -2719,12 +2790,20 @@ state_changed(NMDeviceIwd *self, const char *new_state)
               "IWD is connecting to the wrong AP, %s activation",
               switch_ap ? "replacing" : "aborting");
         cleanup_association_attempt(self, !switch_ap);
+
+        if (!switch_ap) {
+            nm_device_state_changed(device,
+                                    NM_DEVICE_STATE_FAILED,
+                                    NM_DEVICE_STATE_REASON_SUPPLICANT_DISCONNECT);
+            return;
+        }
+
+        priv->assuming = TRUE; /* Don't send Station.Disconnect() */
         nm_device_state_changed(device,
-                                NM_DEVICE_STATE_FAILED,
+                                NM_DEVICE_STATE_DISCONNECTED,
                                 NM_DEVICE_STATE_REASON_SUPPLICANT_DISCONNECT);
-
-        if (switch_ap)
-            assume_connection(self, ap);
+        priv->assuming = FALSE;
+        assume_connection(self, ap);
         return;
     }
 
@@ -3101,7 +3180,7 @@ nm_device_iwd_set_dbus_object(NMDeviceIwd *self, GDBusObject *object)
         goto error;
     }
 
-    adapter_proxy = nm_iwd_manager_get_dbus_interface(nm_iwd_manager_get(),
+    adapter_proxy = nm_iwd_manager_get_dbus_interface(priv->manager,
                                                       g_variant_get_string(value, NULL),
                                                       NM_IWD_WIPHY_INTERFACE);
     if (!adapter_proxy) {
@@ -3278,7 +3357,7 @@ nm_device_iwd_agent_query(NMDeviceIwd *self, GDBusMethodInvocation *invocation)
      * it.  IWD only queries us if it has not saved the PSK (e.g. by policy)
      * or a previous attempt has failed with current secrets so it wants a
      * fresh value.  It doesn't know about agent-owned secrets so whenever
-     * possible and the PSK is saved and not asked from NM.  However if this
+     * possible, the PSK is saved and not asked from NM.  However if this
      * is a new connection it may include all of the needed settings already
      * so allow using these, too.  Connection timestamp is set after
      * activation or after first activation failure (to 0).
@@ -3368,6 +3447,341 @@ nm_device_iwd_network_add_remove(NMDeviceIwd *self, GDBusProxy *network, bool ad
     }
 }
 
+static const NML3ConfigData *
+nm_device_iwd_parse_netconfig(NMDeviceIwd *self, int addr_family, GVariantIter *config_iter)
+{
+    NMDevice                               *device = NM_DEVICE(self);
+    nm_auto_unref_l3cd_init NML3ConfigData *l3cd   = NULL;
+    const char                             *key;
+    GVariant                               *value;
+    NMIPConfigSource                        source       = NM_IP_CONFIG_SOURCE_UNKNOWN;
+    nm_auto_free_variant_iter GVariantIter *address_iter = NULL;
+    nm_auto_free_variant_iter GVariantIter *route_iter   = NULL;
+    nm_auto_free_variant_iter GVariantIter *dns_iter     = NULL;
+    nm_auto_free_variant_iter GVariantIter *domain_iter  = NULL;
+    NMSettingConnectionMdns                 mdns         = NM_SETTING_CONNECTION_MDNS_DEFAULT;
+    const char                             *str_value;
+    GVariantIter                           *entry_iter;
+    uint32_t                                timestamp = nm_utils_get_monotonic_timestamp_sec();
+
+    while (g_variant_iter_next(config_iter, "{&sv}", &key, &value)) {
+        _nm_unused gs_unref_variant GVariant *value_free = value;
+
+        if (nm_streq(key, "Method")) {
+            if (!g_variant_is_of_type(value, G_VARIANT_TYPE_STRING)
+                || source != NM_IP_CONFIG_SOURCE_UNKNOWN)
+                goto param_error;
+
+            str_value = g_variant_get_string(value, NULL);
+            if (nm_streq(str_value, "static"))
+                source = NM_IP_CONFIG_SOURCE_USER;
+            else if (nm_streq(str_value, "auto"))
+                /* Use SOURCE_DHCP as shorthand for the various autoconfiguration protocols */
+                source = NM_IP_CONFIG_SOURCE_DHCP;
+            else
+                _LOGW(LOGD_WIFI, "iwd_parse_netconfig: Uknown Method value \"%s\"", str_value);
+        }
+
+        if (nm_streq(key, "Addresses")) {
+            if (!g_variant_is_of_type(value, G_VARIANT_TYPE("aa{sv}")) || address_iter)
+                goto param_error;
+
+            g_variant_get(value, "aa{sv}", &address_iter);
+        }
+
+        if (nm_streq(key, "Routes")) {
+            if (!g_variant_is_of_type(value, G_VARIANT_TYPE("aa{sv}")) || route_iter)
+                goto param_error;
+
+            g_variant_get(value, "aa{sv}", &route_iter);
+        }
+
+        if (nm_streq(key, "DomainNameServers")) {
+            if (!g_variant_is_of_type(value, G_VARIANT_TYPE_STRING_ARRAY) || dns_iter)
+                goto param_error;
+
+            g_variant_get(value, "as", &dns_iter);
+        }
+
+        if (nm_streq(key, "DomainNames")) {
+            if (!g_variant_is_of_type(value, G_VARIANT_TYPE_STRING_ARRAY) || domain_iter)
+                goto param_error;
+
+            g_variant_get(value, "as", &domain_iter);
+        }
+
+        if (nm_streq(key, "MDNS")) {
+            if (!g_variant_is_of_type(value, G_VARIANT_TYPE_STRING)
+                || mdns != NM_SETTING_CONNECTION_MDNS_DEFAULT)
+                goto param_error;
+
+            str_value = g_variant_get_string(value, NULL);
+            if (nm_streq(str_value, "yes"))
+                mdns = NM_SETTING_CONNECTION_MDNS_YES;
+            else if (nm_streq(str_value, "no"))
+                mdns = NM_SETTING_CONNECTION_MDNS_NO;
+            else if (nm_streq(str_value, "resolve"))
+                mdns = NM_SETTING_CONNECTION_MDNS_RESOLVE;
+            else
+                _LOGW(LOGD_WIFI, "iwd_parse_netconfig: Uknown MDNS value \"%s\"", str_value);
+        }
+    }
+
+    if (!address_iter || !route_iter)
+        goto param_error;
+
+    l3cd = nm_l3_config_data_new(nm_device_get_multi_index(device),
+                                 nm_device_get_ip_ifindex(device),
+                                 source);
+
+    while (g_variant_iter_next(address_iter, "a{sv}", &entry_iter)) {
+        _nm_unused nm_auto_free_variant_iter GVariantIter *entry_iter_free = entry_iter;
+        const char                                        *addr_str        = NULL;
+        NMIPAddr                                           addr_bin;
+        uint8_t                                            plen      = 0;
+        const char                                        *bcast_str = NULL;
+        NMIPAddr                                           bcast_bin = {};
+        uint32_t             valid_lifetime     = NM_PLATFORM_LIFETIME_PERMANENT;
+        uint32_t             preferred_lifetime = NM_PLATFORM_LIFETIME_PERMANENT;
+        NMPlatformIPXAddress a;
+
+        while (g_variant_iter_next(entry_iter, "{&sv}", &key, &value)) {
+            _nm_unused gs_unref_variant GVariant *value_free = value;
+
+            if (nm_streq(key, "Address")) {
+                if (!g_variant_is_of_type(value, G_VARIANT_TYPE_STRING) || addr_str)
+                    goto param_error;
+
+                addr_str = g_variant_get_string(value, NULL);
+                if (inet_pton(addr_family, addr_str, &addr_bin) != 1)
+                    goto param_error;
+            }
+
+            if (nm_streq(key, "PrefixLength")) {
+                if (!g_variant_is_of_type(value, G_VARIANT_TYPE_BYTE))
+                    goto param_error;
+
+                plen = g_variant_get_byte(value);
+            }
+
+            if (nm_streq(key, "Broadcast")) {
+                if (!g_variant_is_of_type(value, G_VARIANT_TYPE_STRING) || bcast_str)
+                    goto param_error;
+
+                bcast_str = g_variant_get_string(value, NULL);
+                if (inet_pton(addr_family, bcast_str, &bcast_bin) != 1)
+                    goto param_error;
+            }
+
+            if (nm_streq(key, "ValidLifetime")) {
+                if (!g_variant_is_of_type(value, G_VARIANT_TYPE_UINT32)
+                    || valid_lifetime != NM_PLATFORM_LIFETIME_PERMANENT)
+                    goto param_error;
+
+                valid_lifetime = g_variant_get_uint32(value);
+            }
+
+            if (nm_streq(key, "PreferredLifetime")) {
+                if (!g_variant_is_of_type(value, G_VARIANT_TYPE_UINT32)
+                    || preferred_lifetime != NM_PLATFORM_LIFETIME_PERMANENT)
+                    goto param_error;
+
+                preferred_lifetime = g_variant_get_uint32(value);
+            }
+        }
+
+        if (addr_str == NULL)
+            goto param_error;
+
+        if (valid_lifetime != NM_PLATFORM_LIFETIME_PERMANENT
+            && preferred_lifetime == NM_PLATFORM_LIFETIME_PERMANENT)
+            preferred_lifetime = valid_lifetime;
+
+        if (addr_family == AF_INET) {
+            a.a4 = (NMPlatformIP4Address){
+                .address      = addr_bin.addr4,
+                .peer_address = addr_bin.addr4,
+                .plen         = plen,
+                .timestamp    = (valid_lifetime != NM_PLATFORM_LIFETIME_PERMANENT) ? timestamp : 0,
+                .lifetime     = valid_lifetime,
+                .preferred    = preferred_lifetime,
+                .addr_source  = source,
+                .use_ip4_broadcast_address = (bcast_str != NULL),
+                .broadcast_address         = bcast_bin.addr4,
+            };
+        } else {
+            a.a6 = (NMPlatformIP6Address){
+                .address     = addr_bin.addr6,
+                .plen        = 128,
+                .timestamp   = (valid_lifetime != NM_PLATFORM_LIFETIME_PERMANENT) ? timestamp : 0,
+                .lifetime    = valid_lifetime,
+                .preferred   = preferred_lifetime,
+                .addr_source = source,
+            };
+        }
+
+        nm_l3_config_data_add_address(l3cd, addr_family, NULL, &a.ax);
+    }
+
+    while (g_variant_iter_next(route_iter, "a{sv}", &entry_iter)) {
+        _nm_unused nm_auto_free_variant_iter GVariantIter *entry_iter_free = entry_iter;
+        const char                                        *dst_addr_str    = NULL;
+        NMIPAddr                                           dst_addr_bin    = {};
+        uint8_t                                            dst_plen        = 0;
+        const char                                        *router_str      = NULL;
+        NMIPAddr                                           router_bin      = {};
+        const char                                        *pref_src_str    = NULL;
+        NMIPAddr                                           pref_src_bin    = {};
+        _nm_unused uint32_t lifetime   = NM_PLATFORM_LIFETIME_PERMANENT;
+        _nm_unused uint32_t priority   = 0;
+        uint8_t             preference = 0;
+        uint32_t            mtu        = 0;
+        NMPlatformIPXRoute  r;
+
+        while (g_variant_iter_next(entry_iter, "{&sv}", &key, &value)) {
+            _nm_unused gs_unref_variant GVariant *value_free = value;
+
+            if (nm_streq(key, "Destination")) {
+                if (!g_variant_is_of_type(value, G_VARIANT_TYPE("(sy)")) || dst_addr_str)
+                    goto param_error;
+
+                g_variant_get(value, "(&sy)", &dst_addr_str, &dst_plen);
+                if (inet_pton(addr_family, dst_addr_str, &dst_addr_bin) != 1)
+                    goto param_error;
+            }
+
+            if (nm_streq(key, "Router")) {
+                if (!g_variant_is_of_type(value, G_VARIANT_TYPE_STRING) || router_str)
+                    goto param_error;
+
+                router_str = g_variant_get_string(value, NULL);
+                if (inet_pton(addr_family, router_str, &router_bin) != 1)
+                    goto param_error;
+            }
+
+            if (nm_streq(key, "PreferredSource")) {
+                if (!g_variant_is_of_type(value, G_VARIANT_TYPE_STRING) || pref_src_str)
+                    goto param_error;
+
+                pref_src_str = g_variant_get_string(value, NULL);
+                if (inet_pton(addr_family, pref_src_str, &pref_src_bin) != 1)
+                    goto param_error;
+            }
+
+            if (nm_streq(key, "Lifetime")) {
+                if (!g_variant_is_of_type(value, G_VARIANT_TYPE_UINT32)
+                    || lifetime != NM_PLATFORM_LIFETIME_PERMANENT)
+                    goto param_error;
+
+                lifetime = g_variant_get_uint32(value);
+            }
+
+            if (nm_streq(key, "Priority")) {
+                if (!g_variant_is_of_type(value, G_VARIANT_TYPE_UINT32))
+                    goto param_error;
+
+                priority = g_variant_get_uint32(value);
+            }
+
+            if (nm_streq(key, "Preference")) {
+                if (!g_variant_is_of_type(value, G_VARIANT_TYPE_BYTE))
+                    goto param_error;
+
+                preference = g_variant_get_byte(value);
+            }
+
+            if (nm_streq(key, "MTU")) {
+                if (!g_variant_is_of_type(value, G_VARIANT_TYPE_UINT32))
+                    goto param_error;
+
+                mtu = g_variant_get_uint32(value);
+            }
+        }
+
+        if (addr_family == AF_INET) {
+            r.r4 = (NMPlatformIP4Route){
+                .network  = dst_addr_str ? dst_addr_bin.addr4 : 0,
+                .plen     = dst_addr_str ? dst_plen : 0,
+                .gateway  = router_str ? router_bin.addr4 : 0,
+                .pref_src = pref_src_str ? pref_src_bin.addr4 : 0,
+                .scope_inv =
+                    nm_platform_route_scope_inv(router_str ? RT_SCOPE_UNIVERSE : RT_SCOPE_LINK),
+            };
+        } else {
+            r.r6 = (NMPlatformIP6Route){
+                .network     = dst_addr_str ? dst_addr_bin.addr6 : nm_ip_addr_zero.addr6,
+                .plen        = dst_addr_str ? dst_plen : 0,
+                .gateway     = router_str ? router_bin.addr6 : nm_ip_addr_zero.addr6,
+                .pref_src    = pref_src_str ? pref_src_bin.addr6 : nm_ip_addr_zero.addr6,
+                .rt_pref     = preference,
+                .r_rtm_flags = RTNH_F_ONLINK,
+            };
+        }
+
+        r.rx.metric_any   = TRUE;
+        r.rx.mtu          = mtu;
+        r.rx.rt_source    = source;
+        r.rx.type_coerced = nm_platform_route_type_coerce(RTN_UNICAST);
+        r.rx.table_any    = TRUE;
+
+        /* TODO: set the lifetime */
+        nm_l3_config_data_add_route(l3cd, addr_family, NULL, &r.rx);
+    }
+
+    if (dns_iter) {
+        while (g_variant_iter_next(dns_iter, "&s", &str_value)) {
+            NMIPAddr dns_bin;
+
+            if (inet_pton(addr_family, str_value, &dns_bin) != 1)
+                goto param_error;
+
+            nm_l3_config_data_add_nameserver_detail(l3cd, addr_family, &dns_bin, NULL);
+            nm_l3_config_data_set_dns_priority(l3cd, addr_family, NM_DNS_PRIORITY_DEFAULT_NORMAL);
+        }
+    }
+
+    if (domain_iter) {
+        while (g_variant_iter_next(domain_iter, "&s", &str_value))
+            nm_l3_config_data_add_search(l3cd, addr_family, str_value);
+    }
+
+    if (mdns != NM_SETTING_CONNECTION_MDNS_DEFAULT)
+        nm_l3_config_data_set_mdns(l3cd, mdns);
+
+    return nm_l3_config_data_ref_and_seal(l3cd);
+
+param_error:
+    return NULL;
+}
+
+bool
+nm_device_iwd_set_netconfig(NMDeviceIwd *self, int addr_family, GVariantIter *config_iter)
+{
+    NMDevice             *device = NM_DEVICE(self);
+    NMDeviceIwdPrivate   *priv   = NM_DEVICE_IWD_GET_PRIVATE(self);
+    const NML3ConfigData *l3cd;
+    NMDeviceState         state = nm_device_get_state(device);
+
+    if (state < NM_DEVICE_STATE_CONFIG || state > NM_DEVICE_STATE_ACTIVATED)
+        return FALSE;
+
+    l3cd = nm_device_iwd_parse_netconfig(self, addr_family, config_iter);
+    if (!l3cd) {
+        _LOGE(LOGD_WIFI, "Malformed netconfig DBus structure");
+        return FALSE;
+    }
+
+    if (state == NM_DEVICE_STATE_CONFIG) {
+        nm_l3_config_data_unref(priv->pending_l3cd_x[NM_IS_IPv4(addr_family)]);
+        priv->pending_l3cd_x[NM_IS_IPv4(addr_family)] = l3cd;
+    } else {
+        nm_device_devip_set_state(device, addr_family, NM_DEVICE_IP_STATE_READY, l3cd);
+        nm_l3_config_data_unref(l3cd);
+    }
+
+    return TRUE;
+}
+
 static void
 autoconnect_changed(NMDevice *device, GParamSpec *pspec, NMDeviceIwd *self)
 {
@@ -3411,7 +3825,7 @@ nm_device_iwd_init(NMDeviceIwd *self)
     g_signal_connect(self, "notify::" NM_DEVICE_AUTOCONNECT, G_CALLBACK(autoconnect_changed), self);
 
     /* Make sure the manager is running */
-    (void) nm_iwd_manager_get();
+    priv->manager = g_object_ref(nm_iwd_manager_get());
 }
 
 NMDevice *
@@ -3443,6 +3857,8 @@ dispose(GObject *object)
     G_OBJECT_CLASS(nm_device_iwd_parent_class)->dispose(object);
 
     nm_assert(c_list_is_empty(&priv->aps_lst_head));
+
+    g_clear_object(&priv->manager);
 }
 
 static void
@@ -3472,12 +3888,14 @@ nm_device_iwd_class_init(NMDeviceIwdClass *klass)
     device_class->set_enabled                 = set_enabled;
     device_class->get_type_description        = get_type_description;
 
-    device_class->act_stage1_prepare = act_stage1_prepare;
-    device_class->act_stage2_config  = act_stage2_config;
-    device_class->get_configured_mtu = get_configured_mtu;
-    device_class->deactivate         = deactivate;
-    device_class->deactivate_async   = deactivate_async;
-    device_class->can_reapply_change = can_reapply_change;
+    device_class->act_stage1_prepare   = act_stage1_prepare;
+    device_class->act_stage2_config    = act_stage2_config;
+    device_class->act_stage3_ip_config = act_stage3_ip_config;
+    device_class->ready_for_ip_config  = ready_for_ip_config;
+    device_class->get_configured_mtu   = get_configured_mtu;
+    device_class->deactivate           = deactivate;
+    device_class->deactivate_async     = deactivate_async;
+    device_class->can_reapply_change   = can_reapply_change;
 
     /* Stage 1 needed only for the set_current_ap() call.  Stage 2 is
      * needed if we're assuming a connection still in the "connecting"
diff --git a/src/core/devices/wifi/nm-device-iwd.h b/src/core/devices/wifi/nm-device-iwd.h
index bb136bf6..8449f7f6 100644
--- a/src/core/devices/wifi/nm-device-iwd.h
+++ b/src/core/devices/wifi/nm-device-iwd.h
@@ -11,7 +11,7 @@
 #include "nm-device-wifi.h"
 
 #define NM_TYPE_DEVICE_IWD (nm_device_iwd_get_type())
-#define NM_DEVICE_IWD(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DEVICE_IWD, NMDeviceIwd))
+#define NM_DEVICE_IWD(obj) (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DEVICE_IWD, NMDeviceIwd))
 #define NM_DEVICE_IWD_CLASS(klass) \
     (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_DEVICE_IWD, NMDeviceIwdClass))
 #define NM_IS_DEVICE_IWD(obj)         (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_DEVICE_IWD))
@@ -46,4 +46,6 @@ void _nm_device_iwd_request_scan(NMDeviceIwd           *self,
 
 void nm_device_iwd_network_add_remove(NMDeviceIwd *device, GDBusProxy *network, bool add);
 
+bool nm_device_iwd_set_netconfig(NMDeviceIwd *device, int addr_family, GVariantIter *config_iter);
+
 #endif /* __NETWORKMANAGER_DEVICE_IWD_H__ */
diff --git a/src/core/devices/wifi/nm-device-olpc-mesh.h b/src/core/devices/wifi/nm-device-olpc-mesh.h
index 79b7fd5d..af3abbc0 100644
--- a/src/core/devices/wifi/nm-device-olpc-mesh.h
+++ b/src/core/devices/wifi/nm-device-olpc-mesh.h
@@ -15,7 +15,7 @@
 
 #define NM_TYPE_DEVICE_OLPC_MESH (nm_device_olpc_mesh_get_type())
 #define NM_DEVICE_OLPC_MESH(obj) \
-    (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DEVICE_OLPC_MESH, NMDeviceOlpcMesh))
+    (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DEVICE_OLPC_MESH, NMDeviceOlpcMesh))
 #define NM_DEVICE_OLPC_MESH_CLASS(klass) \
     (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_DEVICE_OLPC_MESH, NMDeviceOlpcMeshClass))
 #define NM_IS_DEVICE_OLPC_MESH(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_DEVICE_OLPC_MESH))
diff --git a/src/core/devices/wifi/nm-device-wifi-p2p.c b/src/core/devices/wifi/nm-device-wifi-p2p.c
index 4576af95..6c0a4d5e 100644
--- a/src/core/devices/wifi/nm-device-wifi-p2p.c
+++ b/src/core/devices/wifi/nm-device-wifi-p2p.c
@@ -22,7 +22,6 @@
 #include "nm-act-request.h"
 #include "nm-l3-config-data.h"
 #include "nm-manager.h"
-#include "nm-manager.h"
 #include "nm-setting-wifi-p2p.h"
 #include "nm-utils.h"
 #include "nm-wifi-p2p-peer.h"
diff --git a/src/core/devices/wifi/nm-device-wifi-p2p.h b/src/core/devices/wifi/nm-device-wifi-p2p.h
index d1aadd8e..08780464 100644
--- a/src/core/devices/wifi/nm-device-wifi-p2p.h
+++ b/src/core/devices/wifi/nm-device-wifi-p2p.h
@@ -11,7 +11,7 @@
 
 #define NM_TYPE_DEVICE_WIFI_P2P (nm_device_wifi_p2p_get_type())
 #define NM_DEVICE_WIFI_P2P(obj) \
-    (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DEVICE_WIFI_P2P, NMDeviceWifiP2P))
+    (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DEVICE_WIFI_P2P, NMDeviceWifiP2P))
 #define NM_DEVICE_WIFI_P2P_CLASS(klass) \
     (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_DEVICE_WIFI_P2P, NMDeviceWifiP2PClass))
 #define NM_IS_DEVICE_WIFI_P2P(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_DEVICE_WIFI_P2P))
diff --git a/src/core/devices/wifi/nm-device-wifi.c b/src/core/devices/wifi/nm-device-wifi.c
index 8af974d0..171743d0 100644
--- a/src/core/devices/wifi/nm-device-wifi.c
+++ b/src/core/devices/wifi/nm-device-wifi.c
@@ -99,13 +99,13 @@ typedef struct {
     NMSupplMgrCreateIfaceHandle *sup_create_handle;
     NMSupplicantInterface       *sup_iface;
 
+    GSource *scan_kickoff_timeout_source;
+
     gint64 scan_last_complete_msec;
     gint64 scan_periodic_next_msec;
 
     gint64 scan_last_request_started_at_msec;
 
-    guint scan_kickoff_timeout_id;
-
     guint ap_dump_id;
 
     guint periodic_update_id;
@@ -508,7 +508,10 @@ _scan_notify_allowed(NMDeviceWifi *self, NMTernary do_kickoff)
         /* something prohibits scanning. */
     } else if (NM_IN_SET(priv->mode, _NM_802_11_MODE_ADHOC, _NM_802_11_MODE_AP)) {
         /* Don't scan when a an AP or Ad-Hoc connection is active as it will
-         * disrupt connected clients or peers. */
+         * disrupt connected clients or peers.
+         *
+         * Explicit scans are allowed however. */
+        explicit_allowed = TRUE;
     } else if (NM_IN_SET(state, NM_DEVICE_STATE_DISCONNECTED, NM_DEVICE_STATE_FAILED)) {
         /* Can always scan when disconnected */
         explicit_allowed = TRUE;
@@ -659,7 +662,7 @@ supplicant_interface_release(NMDeviceWifi *self)
                                         NM_PENDING_ACTION_WAITING_FOR_SUPPLICANT,
                                         TRUE);
 
-    nm_clear_g_source(&priv->scan_kickoff_timeout_id);
+    nm_clear_g_source_inst(&priv->scan_kickoff_timeout_source);
     nm_clear_g_source_inst(&priv->scan_request_delay_source);
     nm_clear_g_cancellable(&priv->scan_request_cancellable);
 
@@ -1718,9 +1721,9 @@ _scan_kickoff_timeout_cb(gpointer user_data)
     NMDeviceWifi        *self = user_data;
     NMDeviceWifiPrivate *priv = NM_DEVICE_WIFI_GET_PRIVATE(self);
 
-    priv->scan_kickoff_timeout_id = 0;
+    nm_clear_g_source_inst(&priv->scan_kickoff_timeout_source);
     _scan_kickoff(self);
-    return G_SOURCE_REMOVE;
+    return G_SOURCE_CONTINUE;
 }
 
 static void
@@ -1763,13 +1766,13 @@ _scan_kickoff(NMDeviceWifi *self)
                    / 1000),
             (int) ((priv->scan_last_request_started_at_msec + ratelimit_duration_msec - now_msec)
                    % 1000),
-            !priv->scan_kickoff_timeout_id ? ", schedule timeout" : "");
-        if (!priv->scan_kickoff_timeout_id
+            !priv->scan_kickoff_timeout_source ? ", schedule timeout" : "");
+        if (!priv->scan_kickoff_timeout_source
             && (priv->scan_explicit_allowed || priv->scan_periodic_allowed)) {
-            priv->scan_kickoff_timeout_id = g_timeout_add(priv->scan_last_request_started_at_msec
-                                                              + ratelimit_duration_msec - now_msec,
-                                                          _scan_kickoff_timeout_cb,
-                                                          self);
+            priv->scan_kickoff_timeout_source = nm_g_timeout_add_source(
+                priv->scan_last_request_started_at_msec + ratelimit_duration_msec - now_msec,
+                _scan_kickoff_timeout_cb,
+                self);
         }
         return;
     }
@@ -1786,14 +1789,16 @@ _scan_kickoff(NMDeviceWifi *self)
         _LOGT_scan("kickoff: don't scan (rate limited for another %d.%03d sec after previous scan)",
                    timeout_msec / 1000,
                    timeout_msec % 1000);
-        nm_clear_g_source(&priv->scan_kickoff_timeout_id);
-        priv->scan_kickoff_timeout_id = g_timeout_add(timeout_msec, _scan_kickoff_timeout_cb, self);
+        nm_clear_g_source_inst(&priv->scan_kickoff_timeout_source);
+        priv->scan_kickoff_timeout_source =
+            nm_g_timeout_add_source(timeout_msec, _scan_kickoff_timeout_cb, self);
         return;
     }
 
     if (priv->scan_explicit_requested) {
         if (!priv->scan_explicit_allowed) {
-            _LOGT_scan("kickoff: don't scan (explicit scan requested but not allowed)");
+            _LOGT_scan(
+                "kickoff: don't scan (explicit scan requested but not allowed at the moment)");
             return;
         }
         priv->scan_explicit_requested = FALSE;
@@ -1803,7 +1808,7 @@ _scan_kickoff(NMDeviceWifi *self)
             _LOGT_scan("kickoff: don't scan (periodic scan currently not allowed)");
             priv->scan_periodic_next_msec    = 0;
             priv->scan_periodic_interval_sec = 0;
-            nm_clear_g_source(&priv->scan_kickoff_timeout_id);
+            nm_clear_g_source_inst(&priv->scan_kickoff_timeout_source);
             return;
         }
 
@@ -1813,12 +1818,12 @@ _scan_kickoff(NMDeviceWifi *self)
             _LOGT_scan("kickoff: don't scan (periodic scan waiting for another %d.%03d sec%s)",
                        (int) ((priv->scan_periodic_next_msec - now_msec) / 1000),
                        (int) ((priv->scan_periodic_next_msec - now_msec) % 1000),
-                       !priv->scan_kickoff_timeout_id ? ", schedule timeout" : "");
-            if (!priv->scan_kickoff_timeout_id) {
-                priv->scan_kickoff_timeout_id =
-                    g_timeout_add_seconds((priv->scan_periodic_next_msec - now_msec + 999) / 1000,
-                                          _scan_kickoff_timeout_cb,
-                                          self);
+                       !priv->scan_kickoff_timeout_source ? ", schedule timeout" : "");
+            if (!priv->scan_kickoff_timeout_source) {
+                priv->scan_kickoff_timeout_source = nm_g_timeout_add_seconds_source(
+                    (priv->scan_periodic_next_msec - now_msec + 999) / 1000,
+                    _scan_kickoff_timeout_cb,
+                    self);
             }
             return;
         }
diff --git a/src/core/devices/wifi/nm-device-wifi.h b/src/core/devices/wifi/nm-device-wifi.h
index 3a85192e..17893a1b 100644
--- a/src/core/devices/wifi/nm-device-wifi.h
+++ b/src/core/devices/wifi/nm-device-wifi.h
@@ -10,7 +10,8 @@
 #include "devices/nm-device.h"
 
 #define NM_TYPE_DEVICE_WIFI (nm_device_wifi_get_type())
-#define NM_DEVICE_WIFI(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DEVICE_WIFI, NMDeviceWifi))
+#define NM_DEVICE_WIFI(obj) \
+    (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DEVICE_WIFI, NMDeviceWifi))
 #define NM_DEVICE_WIFI_CLASS(klass) \
     (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_DEVICE_WIFI, NMDeviceWifiClass))
 #define NM_IS_DEVICE_WIFI(obj)         (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_DEVICE_WIFI))
diff --git a/src/core/devices/wifi/nm-iwd-manager.c b/src/core/devices/wifi/nm-iwd-manager.c
index 2e0d51e5..0d4daf21 100644
--- a/src/core/devices/wifi/nm-iwd-manager.c
+++ b/src/core/devices/wifi/nm-iwd-manager.c
@@ -47,13 +47,18 @@ typedef struct {
 } KnownNetworkData;
 
 typedef struct {
+    GBytes *ssid;
+    gint64  timestamp;
+} RecentlyMirroredData;
+
+typedef struct {
     NMManager          *manager;
     NMSettings         *settings;
     GCancellable       *cancellable;
     gboolean            running;
     GDBusObjectManager *object_manager;
     guint               agent_id;
-    char               *agent_path;
+    guint               netconfig_agent_id;
     GHashTable         *known_networks;
     NMDeviceIwd        *last_agent_call_device;
     char               *last_state_dir;
@@ -62,6 +67,7 @@ typedef struct {
     GHashTable         *p2p_devices;
     NMIwdWfdInfo        wfd_info;
     guint               wfd_use_count;
+    GSList             *recently_mirrored;
 } NMIwdManagerPrivate;
 
 struct _NMIwdManager {
@@ -271,6 +277,77 @@ return_error:
                                                   "Secrets not available for this connection");
 }
 
+static void
+netconfig_agent_dbus_method_cb(GDBusConnection       *connection,
+                               const char            *sender,
+                               const char            *object_path,
+                               const char            *interface_name,
+                               const char            *method_name,
+                               GVariant              *parameters,
+                               GDBusMethodInvocation *invocation,
+                               gpointer               user_data)
+{
+    NMIwdManager                           *self       = user_data;
+    NMIwdManagerPrivate                    *priv       = NM_IWD_MANAGER_GET_PRIVATE(self);
+    gs_free char                           *name_owner = NULL;
+    const char                             *device_path;
+    gs_unref_object GDBusInterface         *device_obj = NULL;
+    NMDevice                               *device;
+    nm_auto_free_variant_iter GVariantIter *config_iter = NULL;
+    const char                             *ifname;
+    int                                     addr_family;
+
+    /* Be paranoid and check the sender address */
+    name_owner = g_dbus_object_manager_client_get_name_owner(
+        G_DBUS_OBJECT_MANAGER_CLIENT(priv->object_manager));
+    if (!nm_streq0(name_owner, sender))
+        goto return_error;
+
+    if (nm_streq(method_name, "ConfigureIPv4"))
+        addr_family = AF_INET;
+    else if (nm_streq(method_name, "ConfigureIPv6"))
+        addr_family = AF_INET6;
+    else
+        goto return_error;
+
+    g_variant_get(parameters, "(&oa{sv})", &device_path, &config_iter);
+
+    device_obj = g_dbus_object_manager_get_interface(priv->object_manager,
+                                                     device_path,
+                                                     NM_IWD_DEVICE_INTERFACE);
+    if (!device_obj) {
+        _LOGE("netconfig-agent-request: unable to find the device object");
+        goto return_error;
+    }
+
+    ifname = get_property_string_or_null(G_DBUS_PROXY(device_obj), "Name");
+    if (!ifname) {
+        _LOGD("Name not cached for device at %s", device_path);
+        goto return_error;
+    }
+
+    device = nm_manager_get_device(priv->manager, ifname, NM_DEVICE_TYPE_WIFI);
+    if (!device || !NM_IS_DEVICE_IWD(device)) {
+        _LOGD("NM device %s is not an IWD-managed device", ifname);
+        goto return_error;
+    }
+
+    if (nm_device_iwd_set_netconfig(NM_DEVICE_IWD(device), addr_family, config_iter)) {
+        g_dbus_method_invocation_return_value(invocation, g_variant_new("()"));
+        return;
+    }
+
+    _LOGD("netconfig-agent-request: device %s did not handle the IWD Netconfig Agent request",
+          ifname);
+
+return_error:
+    /* IWD doesn't look at the specific error */
+    g_dbus_method_invocation_return_error_literal(invocation,
+                                                  NM_DEVICE_ERROR,
+                                                  NM_DEVICE_ERROR_INVALID_CONNECTION,
+                                                  "Couldn't set netconfig data");
+}
+
 static const GDBusInterfaceInfo iwd_agent_iface_info = NM_DEFINE_GDBUS_INTERFACE_INFO_INIT(
     "net.connman.iwd.Agent",
     .methods = NM_DEFINE_GDBUS_METHOD_INFOS(
@@ -296,36 +373,55 @@ static const GDBusInterfaceInfo iwd_agent_iface_info = NM_DEFINE_GDBUS_INTERFACE
                                     .in_args = NM_DEFINE_GDBUS_ARG_INFOS(
                                         NM_DEFINE_GDBUS_ARG_INFO("reason", "s"), ), ), ), );
 
+static const GDBusInterfaceInfo iwd_netconfig_agent_iface_info =
+    NM_DEFINE_GDBUS_INTERFACE_INFO_INIT(
+        "net.connman.iwd.NetworkConfigurationAgent",
+        .methods = NM_DEFINE_GDBUS_METHOD_INFOS(
+            NM_DEFINE_GDBUS_METHOD_INFO("ConfigureIPv4",
+                                        .in_args = NM_DEFINE_GDBUS_ARG_INFOS(
+                                            NM_DEFINE_GDBUS_ARG_INFO("device", "o"),
+                                            NM_DEFINE_GDBUS_ARG_INFO("config", "a{sv}"), ), ),
+            NM_DEFINE_GDBUS_METHOD_INFO("ConfigureIPv6",
+                                        .in_args = NM_DEFINE_GDBUS_ARG_INFOS(
+                                            NM_DEFINE_GDBUS_ARG_INFO("device", "o"),
+                                            NM_DEFINE_GDBUS_ARG_INFO("config", "a{sv}"), ), ), ), );
+
 static guint
-iwd_agent_export(GDBusConnection *connection, gpointer user_data, char **agent_path, GError **error)
+iwd_agent_export(GDBusConnection *connection, gpointer user_data, GError **error)
 {
     static const GDBusInterfaceVTable vtable = {
         .method_call = agent_dbus_method_cb,
     };
-    char         path[50];
-    unsigned int rnd;
-    guint        id;
 
-    nm_random_get_bytes(&rnd, sizeof(rnd));
-
-    nm_sprintf_buf(path, "/agent/%u", rnd);
+    return g_dbus_connection_register_object(
+        connection,
+        NM_IWD_AGENT_PATH,
+        NM_UNCONST_PTR(GDBusInterfaceInfo, &iwd_agent_iface_info),
+        &vtable,
+        user_data,
+        NULL,
+        error);
+}
 
-    id =
-        g_dbus_connection_register_object(connection,
-                                          path,
-                                          NM_UNCONST_PTR(GDBusInterfaceInfo, &iwd_agent_iface_info),
-                                          &vtable,
-                                          user_data,
-                                          NULL,
-                                          error);
+static guint
+iwd_netconfig_agent_export(GDBusConnection *connection, gpointer user_data, GError **error)
+{
+    static const GDBusInterfaceVTable vtable = {
+        .method_call = netconfig_agent_dbus_method_cb,
+    };
 
-    if (id)
-        *agent_path = g_strdup(path);
-    return id;
+    return g_dbus_connection_register_object(
+        connection,
+        NM_IWD_AGENT_PATH,
+        NM_UNCONST_PTR(GDBusInterfaceInfo, &iwd_netconfig_agent_iface_info),
+        &vtable,
+        user_data,
+        NULL,
+        error);
 }
 
 static void
-register_agent(NMIwdManager *self)
+register_agent(NMIwdManager *self, const char *method)
 {
     NMIwdManagerPrivate *priv = NM_IWD_MANAGER_GET_PRIVATE(self);
     GDBusInterface      *agent_manager;
@@ -334,14 +430,14 @@ register_agent(NMIwdManager *self)
                                                         "/net/connman/iwd", /* IWD 1.0+ */
                                                         NM_IWD_AGENT_MANAGER_INTERFACE);
     if (!agent_manager) {
-        _LOGE("unable to register the IWD Agent: PSK/8021x Wi-Fi networks may not work");
+        _LOGE("unable to register the IWD Agent");
         return;
     }
 
     /* Register our agent */
     g_dbus_proxy_call(G_DBUS_PROXY(agent_manager),
-                      "RegisterAgent",
-                      g_variant_new("(o)", priv->agent_path),
+                      method,
+                      g_variant_new("(o)", NM_IWD_AGENT_PATH),
                       G_DBUS_CALL_FLAGS_NONE,
                       -1,
                       NULL,
@@ -353,6 +449,70 @@ register_agent(NMIwdManager *self)
 
 /*****************************************************************************/
 
+static void
+recently_mirrored_data_free(void *data)
+{
+    RecentlyMirroredData *rmd = data;
+
+    g_bytes_unref(rmd->ssid);
+    g_free(rmd);
+}
+
+/* When we mirror an 802.1x connection to an IWD config file, and there's an
+ * AP in range with matching SSID, that connection should become available
+ * for activation.  In IWD terms when an 802.1x network becomes a Known
+ * Network, it can be connected to using the .Connect D-Bus method.
+ *
+ * However there's a delay between writing the IWD config file and receiving
+ * the InterfaceAdded event for the Known Network so we don't immediately
+ * find out that the network can now be used.  If an NM client creates a
+ * new connection for an 802.1x AP and tries to activate it immediately,
+ * NMDeviceIWD will not allow it to because it doesn't know the network is
+ * known yet.  To work around this, we save the SSIDs of 802.1x connections
+ * we recently mirrored to IWD config files, for 2 seconds, and we treat
+ * them as Known Networks in that period since in theory activations should
+ * succeed.
+ */
+bool
+nm_iwd_manager_is_recently_mirrored(NMIwdManager *self, const GBytes *ssid)
+{
+    NMIwdManagerPrivate  *priv = NM_IWD_MANAGER_GET_PRIVATE(self);
+    gint64                now  = nm_utils_get_monotonic_timestamp_nsec();
+    GSList               *iter;
+    RecentlyMirroredData *rmd;
+
+    /* Drop entries older than 2 seconds */
+    while (priv->recently_mirrored) {
+        rmd = priv->recently_mirrored->data;
+        if (now < rmd->timestamp + 2000000000)
+            break;
+
+        priv->recently_mirrored = g_slist_remove(priv->recently_mirrored, rmd);
+        recently_mirrored_data_free(rmd);
+    }
+
+    for (iter = priv->recently_mirrored; iter; iter = iter->next) {
+        rmd = iter->data;
+        if (g_bytes_equal(ssid, rmd->ssid))
+            return TRUE;
+    }
+
+    return FALSE;
+}
+
+static void
+save_mirrored(NMIwdManager *self, GBytes *ssid)
+{
+    NMIwdManagerPrivate  *priv = NM_IWD_MANAGER_GET_PRIVATE(self);
+    RecentlyMirroredData *rmd  = g_malloc(sizeof(RecentlyMirroredData));
+
+    rmd->ssid               = g_bytes_ref(ssid);
+    rmd->timestamp          = nm_utils_get_monotonic_timestamp_nsec();
+    priv->recently_mirrored = g_slist_append(priv->recently_mirrored, rmd);
+}
+
+/*****************************************************************************/
+
 static KnownNetworkId *
 known_network_id_new(const char *name, NMIwdNetworkSecurity security)
 {
@@ -721,6 +881,9 @@ sett_conn_changed(NMSettingsConnection   *sett_conn,
                "iwd: changed Wi-Fi connection %s mirrored as IWD profile %s",
                nm_settings_connection_get_id(sett_conn),
                full_path);
+
+    if (security == NM_IWD_NETWORK_SECURITY_8021X)
+        save_mirrored(nm_iwd_manager_get(), ssid);
 }
 
 /* Look up an existing NMSettingsConnection for a network that has been
@@ -1283,6 +1446,7 @@ connection_added(NMSettings *settings, NMSettingsConnection *sett_conn, gpointer
     gs_free_error GError           *error      = NULL;
     nm_auto_unref_keyfile GKeyFile *iwd_config = NULL;
     NMSettingsConnectionIntFlags    flags;
+    NMIwdNetworkSecurity            security;
 
     if (!nm_streq(nm_settings_connection_get_connection_type(sett_conn), "802-11-wireless"))
         return;
@@ -1338,6 +1502,12 @@ connection_added(NMSettings *settings, NMSettingsConnection *sett_conn, gpointer
     _LOGD("New Wi-Fi connection %s mirrored as IWD profile %s",
           nm_settings_connection_get_id(sett_conn),
           full_path);
+
+    if (nm_wifi_connection_get_iwd_ssid_and_security(conn, NULL, &security)
+        && security == NM_IWD_NETWORK_SECURITY_8021X) {
+        NMSettingWireless *s_wifi = nm_connection_get_setting_wireless(conn);
+        save_mirrored(nm_iwd_manager_get(), nm_setting_wireless_get_ssid(s_wifi));
+    }
 }
 
 static gboolean
@@ -1355,26 +1525,33 @@ _om_has_name_owner(GDBusObjectManager *object_manager)
 static void
 release_object_manager(NMIwdManager *self)
 {
-    NMIwdManagerPrivate *priv = NM_IWD_MANAGER_GET_PRIVATE(self);
+    NMIwdManagerPrivate      *priv = NM_IWD_MANAGER_GET_PRIVATE(self);
+    GDBusConnection          *agent_connection;
+    GDBusObjectManagerClient *omc;
 
     if (!priv->object_manager)
         return;
 
     g_signal_handlers_disconnect_by_data(priv->object_manager, self);
 
-    if (priv->agent_id) {
-        GDBusConnection          *agent_connection;
-        GDBusObjectManagerClient *omc = G_DBUS_OBJECT_MANAGER_CLIENT(priv->object_manager);
-
-        agent_connection = g_dbus_object_manager_client_get_connection(omc);
+    omc              = G_DBUS_OBJECT_MANAGER_CLIENT(priv->object_manager);
+    agent_connection = g_dbus_object_manager_client_get_connection(omc);
 
-        /* We're is called when we're shutting down (i.e. our DBus connection
-         * is being closed, and IWD will detect this) or IWD was stopped so
-         * in either case calling UnregisterAgent will not do anything.
-         */
+    /* We're called when we're shutting down (i.e. our DBus connection
+     * is being closed, and IWD will detect this) or IWD was stopped so
+     * in either case calling UnregisterAgent will not do anything.
+     * Just unregister the agent interfaces.  The agents are on the same
+     * object (same path) but it seems g_dbus_connection_unregister_object()
+     * should be called for each interface on the object separately.
+     */
+    if (priv->agent_id) {
         g_dbus_connection_unregister_object(agent_connection, priv->agent_id);
         priv->agent_id = 0;
-        nm_clear_g_free(&priv->agent_path);
+    }
+
+    if (priv->netconfig_agent_id) {
+        g_dbus_connection_unregister_object(agent_connection, priv->netconfig_agent_id);
+        priv->netconfig_agent_id = 0;
     }
 
     g_clear_object(&priv->object_manager);
@@ -1589,6 +1766,10 @@ next:
     }
 
     g_variant_iter_free(properties_iter);
+
+    /* Register the netconfig agent only once we know netconfig is enabled */
+    if (nm_iwd_manager_get_netconfig_enabled(self) && priv->netconfig_agent_id)
+        register_agent(self, "RegisterNetworkConfigurationAgent");
 }
 
 static void
@@ -1620,13 +1801,19 @@ got_object_manager(GObject *object, GAsyncResult *result, gpointer user_data)
     connection =
         g_dbus_object_manager_client_get_connection(G_DBUS_OBJECT_MANAGER_CLIENT(object_manager));
 
-    priv->agent_id = iwd_agent_export(connection, self, &priv->agent_path, &error);
+    priv->agent_id = iwd_agent_export(connection, self, &error);
     if (!priv->agent_id) {
         _LOGE("failed to export the IWD Agent: PSK/8021x Wi-Fi networks may not work: %s",
               error->message);
         g_clear_error(&error);
     }
 
+    priv->netconfig_agent_id = iwd_netconfig_agent_export(connection, self, &error);
+    if (!priv->netconfig_agent_id) {
+        _LOGE("failed to export the IWD Netconfig Agent: %s", error->message);
+        g_clear_error(&error);
+    }
+
     if (_om_has_name_owner(object_manager)) {
         GList                          *objects, *iter;
         gs_unref_object GDBusInterface *daemon = NULL;
@@ -1654,7 +1841,7 @@ got_object_manager(GObject *object, GAsyncResult *result, gpointer user_data)
         g_list_free_full(objects, g_object_unref);
 
         if (priv->agent_id)
-            register_agent(self);
+            register_agent(self, "RegisterAgent");
 
         priv->netconfig_enabled = false; /* Assume false until GetInfo() results come in */
 
@@ -1952,6 +2139,8 @@ dispose(GObject *object)
 
     g_hash_table_unref(nm_steal_pointer(&priv->p2p_devices));
 
+    g_slist_free_full(nm_steal_pointer(&priv->recently_mirrored), recently_mirrored_data_free);
+
     G_OBJECT_CLASS(nm_iwd_manager_parent_class)->dispose(object);
 }
 
diff --git a/src/core/devices/wifi/nm-iwd-manager.h b/src/core/devices/wifi/nm-iwd-manager.h
index 02cd6bba..80123c9a 100644
--- a/src/core/devices/wifi/nm-iwd-manager.h
+++ b/src/core/devices/wifi/nm-iwd-manager.h
@@ -30,8 +30,11 @@
 #define NM_IWD_P2P_SERVICE_MANAGER_INTERFACE "net.connman.iwd.p2p.ServiceManager"
 #define NM_IWD_P2P_WFD_INTERFACE             "net.connman.iwd.p2p.Display"
 
+#define NM_IWD_AGENT_PATH NM_DBUS_PATH "/iwd/agent"
+
 #define NM_TYPE_IWD_MANAGER (nm_iwd_manager_get_type())
-#define NM_IWD_MANAGER(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_IWD_MANAGER, NMIwdManager))
+#define NM_IWD_MANAGER(obj) \
+    (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_IWD_MANAGER, NMIwdManager))
 #define NM_IWD_MANAGER_CLASS(klass) \
     (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_IWD_MANAGER, NMIwdManagerClass))
 #define NM_IS_IWD_MANAGER(obj)         (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_IWD_MANAGER))
@@ -63,4 +66,6 @@ gboolean nm_iwd_manager_check_wfd_info_compatible(NMIwdManager *self, const NMIw
 gboolean nm_iwd_manager_register_wfd(NMIwdManager *self, const NMIwdWfdInfo *wfd_info);
 void     nm_iwd_manager_unregister_wfd(NMIwdManager *self);
 
+bool nm_iwd_manager_is_recently_mirrored(NMIwdManager *self, const GBytes *ssid);
+
 #endif /* __NETWORKMANAGER_IWD_MANAGER_H__ */
diff --git a/src/core/devices/wifi/nm-wifi-ap.h b/src/core/devices/wifi/nm-wifi-ap.h
index c0422900..93dc8edf 100644
--- a/src/core/devices/wifi/nm-wifi-ap.h
+++ b/src/core/devices/wifi/nm-wifi-ap.h
@@ -13,7 +13,7 @@
 #include "libnm-base/nm-base.h"
 
 #define NM_TYPE_WIFI_AP            (nm_wifi_ap_get_type())
-#define NM_WIFI_AP(obj)            (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_WIFI_AP, NMWifiAP))
+#define NM_WIFI_AP(obj)            (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_WIFI_AP, NMWifiAP))
 #define NM_WIFI_AP_CLASS(klass)    (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_WIFI_AP, NMWifiAPClass))
 #define NM_IS_WIFI_AP(obj)         (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_WIFI_AP))
 #define NM_IS_WIFI_AP_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), NM_TYPE_WIFI_AP))
diff --git a/src/core/devices/wifi/nm-wifi-factory.c b/src/core/devices/wifi/nm-wifi-factory.c
index 5e354c6b..19880a90 100644
--- a/src/core/devices/wifi/nm-wifi-factory.c
+++ b/src/core/devices/wifi/nm-wifi-factory.c
@@ -24,7 +24,7 @@
 
 #define NM_TYPE_WIFI_FACTORY (nm_wifi_factory_get_type())
 #define NM_WIFI_FACTORY(obj) \
-    (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_WIFI_FACTORY, NMWifiFactory))
+    (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_WIFI_FACTORY, NMWifiFactory))
 #define NM_WIFI_FACTORY_CLASS(klass) \
     (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_WIFI_FACTORY, NMWifiFactoryClass))
 #define NM_IS_WIFI_FACTORY(obj)         (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_WIFI_FACTORY))
diff --git a/src/core/devices/wifi/nm-wifi-p2p-peer.h b/src/core/devices/wifi/nm-wifi-p2p-peer.h
index 5124d1de..b0abeb37 100644
--- a/src/core/devices/wifi/nm-wifi-p2p-peer.h
+++ b/src/core/devices/wifi/nm-wifi-p2p-peer.h
@@ -12,7 +12,7 @@
 
 #define NM_TYPE_WIFI_P2P_PEER (nm_wifi_p2p_peer_get_type())
 #define NM_WIFI_P2P_PEER(obj) \
-    (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_WIFI_P2P_PEER, NMWifiP2PPeer))
+    (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_WIFI_P2P_PEER, NMWifiP2PPeer))
 #define NM_WIFI_P2P_PEER_CLASS(klass) \
     (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_WIFI_P2P_PEER, NMWifiP2PPeerClass))
 #define NM_IS_WIFI_P2P_PEER(obj)         (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_WIFI_P2P_PEER))
diff --git a/src/core/devices/wifi/nm-wifi-utils.c b/src/core/devices/wifi/nm-wifi-utils.c
index 1a8204b0..8a8d062f 100644
--- a/src/core/devices/wifi/nm-wifi-utils.c
+++ b/src/core/devices/wifi/nm-wifi-utils.c
@@ -11,6 +11,7 @@
 #include <netinet/if_ether.h>
 #include <stdlib.h>
 
+#include "libnm-glib-aux/nm-str-buf.h"
 #include "nm-utils.h"
 #include "libnm-core-intern/nm-core-internal.h"
 #include "libnm-core-aux-intern/nm-libnm-core-utils.h"
@@ -812,13 +813,12 @@ nm_wifi_utils_complete_connection(GBytes       *ap_ssid,
          * setting.  Since there's so much configuration required for it, there's
          * no way it can be automatically completed.
          */
-    } else if (nm_streq0(key_mgmt, "wpa-psk")
-               || (ap_rsn_flags & NM_802_11_AP_SEC_KEY_MGMT_SAE
-                   && (ap_wpa_flags & NM_802_11_AP_SEC_KEY_MGMT_PSK
-                       || ap_rsn_flags & NM_802_11_AP_SEC_KEY_MGMT_PSK))) {
+    } else if (ap_rsn_flags & NM_802_11_AP_SEC_KEY_MGMT_SAE
+               && (ap_wpa_flags & NM_802_11_AP_SEC_KEY_MGMT_PSK
+                   || ap_rsn_flags & NM_802_11_AP_SEC_KEY_MGMT_PSK)) {
         g_object_set(s_wsec,
                      NM_SETTING_WIRELESS_SECURITY_KEY_MGMT,
-                     "wpa-psk",
+                     nm_streq0(key_mgmt, "sae") ? "sae" : "wpa-psk",
                      NM_SETTING_WIRELESS_SECURITY_AUTH_ALG,
                      "open",
                      NULL);
@@ -828,7 +828,7 @@ nm_wifi_utils_complete_connection(GBytes       *ap_ssid,
                || NM_FLAGS_ANY(ap_rsn_flags,
                                NM_802_11_AP_SEC_KEY_MGMT_OWE | NM_802_11_AP_SEC_KEY_MGMT_OWE_TM)) {
         g_object_set(s_wsec, NM_SETTING_WIRELESS_SECURITY_KEY_MGMT, "owe", NULL);
-    } else if (ap_wpa_flags & NM_802_11_AP_SEC_KEY_MGMT_PSK
+    } else if (nm_streq0(key_mgmt, "wpa-psk") || ap_wpa_flags & NM_802_11_AP_SEC_KEY_MGMT_PSK
                || ap_rsn_flags & NM_802_11_AP_SEC_KEY_MGMT_PSK) {
         g_object_set(s_wsec,
                      NM_SETTING_WIRELESS_SECURITY_KEY_MGMT,
@@ -1004,7 +1004,7 @@ psk_setting_to_iwd_config(GKeyFile *file, NMSettingWirelessSecurity *s_wsec, GEr
         if (NM_FLAGS_ANY(psk_flags, SECRETS_DONT_STORE_FLAGS)) {
             nm_log_info(
                 LOGD_WIFI,
-                "IWD network config is being created wihout the PSK but IWD will save the PSK on "
+                "IWD network config is being created without the PSK but IWD will save the PSK on "
                 "successful activation not honoring the psk-flags property");
         }
         return TRUE;
@@ -1549,10 +1549,16 @@ eap_setting_to_iwd_config(GKeyFile *file, NMSetting8021x *s_8021x, GError **erro
 }
 
 static gboolean
-ip4_config_to_iwd_config(GKeyFile *file, NMSettingIPConfig *s_ip, GError **error)
+ip_config_to_iwd_config(int addr_family, GKeyFile *file, NMSettingIPConfig *s_ip, GError **error)
 {
-    guint          num;
-    struct in_addr ip;
+    const int                IS_IPv4 = NM_IS_IPv4(addr_family);
+    nm_auto_str_buf NMStrBuf strbuf  = NM_STR_BUF_INIT_A(NM_UTILS_GET_NEXT_REALLOC_SIZE_488, FALSE);
+    NMIPAddress             *addr;
+    guint                    num;
+    guint                    i;
+    char                     buf[NM_INET_ADDRSTRLEN + 10];
+    const char              *kf_group = IS_IPv4 ? "IPv4" : "IPv6";
+    const char              *gw;
 
     /* These settings are not acutally used unless global
      * [General].EnableNetworkConfiguration is true, which we don't support.
@@ -1564,15 +1570,26 @@ ip4_config_to_iwd_config(GKeyFile *file, NMSettingIPConfig *s_ip, GError **error
     if (!s_ip)
         return TRUE;
 
+    nm_assert(NM_IS_IPv4(addr_family) ? NM_IS_SETTING_IP4_CONFIG(s_ip)
+                                      : NM_IS_SETTING_IP6_CONFIG(s_ip));
+
     num = nm_setting_ip_config_get_num_dns(s_ip);
     if (num) {
-        nm_auto_free_gstring GString *s = g_string_sized_new(128);
-        guint                         i;
-
+        nm_str_buf_reset(&strbuf);
         for (i = 0; i < num; i++) {
-            if (s->len)
-                g_string_append_c(s, ' ');
-            g_string_append(s, nm_setting_ip_config_get_dns(s_ip, i));
+            char     sbuf[NM_INET_ADDRSTRLEN];
+            NMIPAddr a;
+
+            if (!nm_utils_dnsname_parse_assert(addr_family,
+                                               nm_setting_ip_config_get_dns(s_ip, i),
+                                               NULL,
+                                               &a,
+                                               NULL))
+                continue;
+
+            if (strbuf.len > 0)
+                nm_str_buf_append_c(&strbuf, ' ');
+            nm_str_buf_append(&strbuf, nm_inet_ntop(addr_family, &a, sbuf));
         }
         /* It doesn't matter whether we add the DNS under [IPv4] or [IPv6]
          * except that with method=auto the list will override the
@@ -1581,97 +1598,61 @@ ip4_config_to_iwd_config(GKeyFile *file, NMSettingIPConfig *s_ip, GError **error
          * Note ignore-auto-dns=false isn't supported, this list always
          * overrides the DHCP DNSes.
          */
-        g_key_file_set_string(file, "IPv4", "DNS", s->str);
+        g_key_file_set_string(file, kf_group, "DNS", nm_str_buf_get_str(&strbuf));
     }
 
-    if (!nm_streq0(nm_setting_ip_config_get_method(s_ip), NM_SETTING_IP4_CONFIG_METHOD_MANUAL))
-        return TRUE;
-
-    num = nm_setting_ip_config_get_num_addresses(s_ip);
-    if (num) {
-        NMIPAddress *addr    = nm_setting_ip_config_get_address(s_ip, 0);
-        guint        prefix  = nm_ip_address_get_prefix(addr);
-        in_addr_t    netmask = _nm_utils_ip4_prefix_to_netmask(prefix);
-        char         buf[INET_ADDRSTRLEN];
-
-        nm_ip_address_get_address_binary(addr, &ip);
-        g_key_file_set_string(file, "IPv4", "Address", nm_ip_address_get_address(addr));
-        g_key_file_set_string(file, "IPv4", "Netmask", _nm_utils_inet4_ntop(netmask, buf));
-    } else {
-        inet_pton(AF_INET, "10.42.0.100", &ip);
-        g_key_file_set_string(file, "IPv4", "Address", "10.42.0.100");
+    if (!IS_IPv4) {
+        if (!NM_IN_STRSET(nm_setting_ip_config_get_method(s_ip),
+                          NM_SETTING_IP6_CONFIG_METHOD_AUTO,
+                          NM_SETTING_IP6_CONFIG_METHOD_DHCP,
+                          NM_SETTING_IP6_CONFIG_METHOD_MANUAL))
+            return TRUE;
+        g_key_file_set_boolean(file, kf_group, "Enabled", TRUE);
     }
 
-    if (nm_setting_ip_config_get_gateway(s_ip)) {
-        g_key_file_set_string(file, "IPv4", "Gateway", nm_setting_ip_config_get_gateway(s_ip));
-    } else {
-        uint32_t val;
-        char     buf[INET_ADDRSTRLEN];
-
-        /* IWD won't enable static IP unless both Address and Gateway are
-         * set so generate a gateway address if not known.
-         */
-        val = (ntohl(ip.s_addr) & 0xfffffff0) + 1;
-        if (val == ntohl(ip.s_addr))
-            val += 1;
-        g_key_file_set_string(file, "IPv4", "Gateway", _nm_utils_inet4_ntop(htonl(val), buf));
-    }
-
-    return TRUE;
-}
-
-static gboolean
-ip6_config_to_iwd_config(GKeyFile *file, NMSettingIPConfig *s_ip, GError **error)
-{
-    guint        num;
-    NMIPAddress *addr;
-    char         buf[INET6_ADDRSTRLEN + 10];
-
-    if (!s_ip)
+    num = nm_setting_ip_config_get_num_addresses(s_ip);
+    if (num == 0)
         return TRUE;
 
-    num = nm_setting_ip_config_get_num_dns(s_ip);
-    if (num) {
-        nm_auto_free_gstring GString *s = g_string_sized_new(128);
-        guint                         i;
+    addr = nm_setting_ip_config_get_address(s_ip, 0);
+    gw   = nm_setting_ip_config_get_gateway(s_ip);
 
-        for (i = 0; i < num; i++) {
-            if (s->len)
-                g_string_append_c(s, ' ');
-            g_string_append(s, nm_setting_ip_config_get_dns(s_ip, i));
-        }
-        g_key_file_set_string(file, "IPv6", "DNS", s->str);
-    }
+    if (IS_IPv4) {
+        in_addr_t ip;
 
-    if (!NM_IN_STRSET(nm_setting_ip_config_get_method(s_ip),
-                      NM_SETTING_IP6_CONFIG_METHOD_AUTO,
-                      NM_SETTING_IP6_CONFIG_METHOD_DHCP,
-                      NM_SETTING_IP6_CONFIG_METHOD_MANUAL))
-        return TRUE;
+        nm_ip_address_get_address_binary(addr, &ip);
 
-    g_key_file_set_boolean(file, "IPv6", "Enabled", TRUE);
+        g_key_file_set_string(file, kf_group, "Address", nm_ip_address_get_address(addr));
+        g_key_file_set_string(
+            file,
+            kf_group,
+            "Netmask",
+            nm_inet4_ntop(nm_ip4_addr_netmask_from_prefix(nm_ip_address_get_prefix(addr)), buf));
 
-    if (!nm_streq0(nm_setting_ip_config_get_method(s_ip), NM_SETTING_IP6_CONFIG_METHOD_MANUAL))
-        return TRUE;
+        if (!gw) {
+            guint32 val;
 
-    if (!nm_setting_ip_config_get_num_addresses(s_ip)) {
-        g_set_error_literal(error,
-                            NM_CONNECTION_ERROR,
-                            NM_CONNECTION_ERROR_INVALID_PROPERTY,
-                            "IP address required for IPv6 manual config");
-        return FALSE;
+            /* IWD won't enable static IP unless both Address and Gateway are
+             * set so generate a gateway address if not known.
+             */
+            val = (ntohl(ip) & 0xfffffff0) + 1;
+            if (val == ntohl(ip))
+                val += 1;
+            gw = nm_inet4_ntop(htonl(val), buf);
+        }
+        g_key_file_set_string(file, kf_group, "Gateway", gw);
+    } else {
+        g_key_file_set_string(file,
+                              kf_group,
+                              "Address",
+                              nm_sprintf_buf(buf,
+                                             "%s/%u",
+                                             nm_ip_address_get_address(addr),
+                                             nm_ip_address_get_prefix(addr)));
+        if (gw)
+            g_key_file_set_string(file, kf_group, "Gateway", gw);
     }
 
-    addr = nm_setting_ip_config_get_address(s_ip, 0);
-    g_key_file_set_string(file,
-                          "IPv6",
-                          "Address",
-                          nm_sprintf_buf(buf,
-                                         "%s/%u",
-                                         nm_ip_address_get_address(addr),
-                                         nm_ip_address_get_prefix(addr)));
-    if (nm_setting_ip_config_get_gateway(s_ip))
-        g_key_file_set_string(file, "IPv6", "Gateway", nm_setting_ip_config_get_gateway(s_ip));
     return TRUE;
 }
 
@@ -1759,13 +1740,15 @@ nm_wifi_utils_connection_to_iwd_config(NMConnection *connection,
     else if (cloned_mac_addr && nm_utils_hwaddr_valid(cloned_mac_addr, ETH_ALEN))
         g_key_file_set_string(file, "Settings", "AddressOverride", cloned_mac_addr);
 
-    if (!ip4_config_to_iwd_config(
+    if (!ip_config_to_iwd_config(
+            AF_INET,
             file,
             NM_SETTING_IP_CONFIG(nm_connection_get_setting_ip4_config(connection)),
             error))
         return NULL;
 
-    if (!ip6_config_to_iwd_config(
+    if (!ip_config_to_iwd_config(
+            AF_INET6,
             file,
             NM_SETTING_IP_CONFIG(nm_connection_get_setting_ip6_config(connection)),
             error))