about summary refs log tree commit diff
path: root/src/core/devices/wifi/nm-device-iwd.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/core/devices/wifi/nm-device-iwd.c')
-rw-r--r--src/core/devices/wifi/nm-device-iwd.c466
1 files changed, 442 insertions, 24 deletions
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"