about summary refs log tree commit diff
diff options
context:
space:
mode:
authorMichael Biebl <biebl@debian.org>2025-02-28 17:38:21 +0100
committerMichael Biebl <biebl@debian.org>2025-02-28 17:38:21 +0100
commit625e8ce60e826bde943487b8238884a9232e4562 (patch)
tree4c1f0f88c466c69c27fc9ffbe787cc7d0964d630
parent8bdf070ff046f482f6eb5e2b15ebc216f5d1e3da (diff)
New upstream version 1.52.0 upstream/1.52.0
-rw-r--r--.gitlab-ci.yml4
-rwxr-xr-x.gitlab-ci/fedora-install.sh4
-rw-r--r--NEWS13
-rwxr-xr-xcontrib/fedora/REQUIRED_PACKAGES16
-rwxr-xr-xcontrib/fedora/rpm/build.sh4
-rwxr-xr-xcontrib/scripts/nm-ci-patch-gtkdoc.sh40
-rw-r--r--meson.build2
-rw-r--r--src/core/dns/nm-dns-dnsconfd.c124
-rw-r--r--src/core/nm-l3cfg.c69
-rw-r--r--src/core/nm-manager.c164
-rw-r--r--src/core/nm-netns.c19
-rw-r--r--src/core/nm-netns.h4
-rw-r--r--src/core/nm-policy.c35
-rw-r--r--src/core/settings/plugins/keyfile/nms-keyfile-plugin.c13
-rw-r--r--src/nm-cloud-setup/main.c388
-rw-r--r--src/nm-cloud-setup/nmcs-provider-oci.c77
-rw-r--r--src/nm-cloud-setup/nmcs-provider.c9
-rw-r--r--src/nm-cloud-setup/nmcs-provider.h6
-rw-r--r--src/nmtui/nmt-page-ip6.c1
-rwxr-xr-xsrc/tests/client/test-client.py117
20 files changed, 775 insertions, 334 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 75816296..c3c36ed8 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -61,9 +61,9 @@ variables:
   # This is done by running `ci-fairy generate-template` and possibly bumping
   # ".default_tag".
   ALPINE_TAG:  'tag-77ec3d923fd6'
-  CENTOS_TAG:  'tag-8af9c6a05c7b'
+  CENTOS_TAG:  'tag-7a677f4838e1'
   DEBIAN_TAG:  'tag-ecad19904683'
-  FEDORA_TAG:  'tag-8af9c6a05c7b'
+  FEDORA_TAG:  'tag-7a677f4838e1'
   UBUNTU_TAG:  'tag-ecad19904683'
 
   ALPINE_EXEC: 'bash .gitlab-ci/alpine-install.sh'
diff --git a/.gitlab-ci/fedora-install.sh b/.gitlab-ci/fedora-install.sh
index 3bd46983..50f9710f 100755
--- a/.gitlab-ci/fedora-install.sh
+++ b/.gitlab-ci/fedora-install.sh
@@ -39,7 +39,7 @@ fi
 fi
 
 
-NM_NO_EXTRA=1 NM_INSTALL="yum install -y" ./contrib/fedora/REQUIRED_PACKAGES
+NM_NO_EXTRA=1 NM_INSTALL="yum install -y --allowerasing" ./contrib/fedora/REQUIRED_PACKAGES
 yum install -y glibc-langpack-pl ccache clang
 
 # containers have "tsflags=nodocs" in /etc/dnf/dnf.conf. We need /usr/shared/gtk-doc/html
@@ -66,8 +66,6 @@ else
     debuginfo-install -y glib2
 fi
 
-contrib/scripts/nm-ci-patch-gtkdoc.sh || true
-
 if [ -x /usr/bin/ninja ] && ! [ -x /usr/bin/ninja-build ]; then
     ln -s /usr/bin/ninja-build /usr/bin/ninja
 fi
diff --git a/NEWS b/NEWS
index da99a777..1e50ee0c 100644
--- a/NEWS
+++ b/NEWS
@@ -1,13 +1,8 @@
 =============================================
-NetworkManager-1.52-rc1
+NetworkManager-1.52
 Overview of changes since NetworkManager-1.50
 =============================================
 
-This is a NetworkManager release candidate. The API is
-subject to change and not guaranteed to be compatible with
-the later release.
-USE AT YOUR OWN RISK.  NOT RECOMMENDED FOR PRODUCTION USE!
-
 * Store interactive history in `$XDG_CACHE_HOME/nmcli-history` instead of
   `~/.nmcli-history`
 * Add new ipv4.link-local=fallback to set an IPv4 link-local address
@@ -37,6 +32,12 @@ USE AT YOUR OWN RISK.  NOT RECOMMENDED FOR PRODUCTION USE!
 * Dnsconfd plugin can now be used for configuration of system-wide DNS
   caching resolver. If dnsconfd plugin is enabled and ipvX.routed-dns is
   set to -1 then adding routes is by default enabled.
+* Add "shared" method to the IPv6 configuration options in nmtui.
+* Fix a bug that prevented the activation of bond and bridge's ports in some cases
+* Fix a bug that prevented the activation of OVS interfaces in some cases
+* Fix MTPCP endpoint creation for IPv4 with DAD and IPv6 tentative addresses
+* Fix some VPN routes not being added to the table specified in ipv4/6.routing-table.
+  This fix allow to use policy routing to mitigate Tunnelvision attacks.
 
 =============================================
 NetworkManager-1.50
diff --git a/contrib/fedora/REQUIRED_PACKAGES b/contrib/fedora/REQUIRED_PACKAGES
index d47e6098..1bb7ec6b 100755
--- a/contrib/fedora/REQUIRED_PACKAGES
+++ b/contrib/fedora/REQUIRED_PACKAGES
@@ -25,6 +25,12 @@ install() {
     fi
 }
 
+install_ignore_missing() {
+    for p; do
+        install "$p" || :
+    done
+}
+
 if test "$NM_NO_EXTRA" != 1; then
     # these packages are convenient for developing, but not necessary
     # for CI testing.
@@ -82,21 +88,15 @@ install \
     which \
     #end
 
-# Some packages don't exist in certain distributions. Ignore errors
-# installing them, but still drag them in when available.
-install --skip-unavailable \
+# some packages don't exist in certain distributions. Install them one-by-one, and ignore errors.
+install_ignore_missing \
     black \
-    dbus-python \
     dhclient \
     iproute-tc \
     libasan \
     libpsl-devel \
     libubsan \
     libvala-devel \
-    pexpect \
-    pygobject3-base \
-    python-gobject-base \
-    python36-pexpect \
     qt-devel \
     teamd-devel \
     vala-devel \
diff --git a/contrib/fedora/rpm/build.sh b/contrib/fedora/rpm/build.sh
index f46b9ba5..4a89a6b4 100755
--- a/contrib/fedora/rpm/build.sh
+++ b/contrib/fedora/rpm/build.sh
@@ -127,7 +127,7 @@ if [ -n "$SOURCE" ]; then
     [[ "$SOURCE_FROM_GIT" == 1 ]] && die "Cannot set both \$SOURCE and \$SOURCE_FROM_GIT=1"
     SOURCE_FROM_GIT=0
 elif [[ "$SOURCE_FROM_GIT" != "1" ]]; then
-    SOURCE="$(ls -1 "$GITDIR/NetworkManager-${VERSION}.tar."* 2>/dev/null | head -n1)"
+    SOURCE="$(ls -1 "$GITDIR/NetworkManager-${VERSION}.tar."* 2>/dev/null | head -n1 || :)"
     if [[ -z "$SOURCE" ]]; then
         [[ "$SOURCE_FROM_GIT" == "0" ]] && die "Either set \$SOURCE or set \$SOURCE_FROM_GIT=1"
         SOURCE_FROM_GIT=1
@@ -237,7 +237,7 @@ rpmbuild --define "_topdir $TEMP" $RPM_BUILD_OPTION "$TEMPSPEC" $NM_RPMBUILD_ARG
 LS_EXTRA=()
 
 if [ "$SIGN_SOURCE" = 1 ]; then
-    SIGNKEY="$(git config --get user.signingkey)"
+    SIGNKEY="$(git config --get user.signingkey || :)"
     if [ "$SIGNKEY" != "" ]; then
         SIGNKEY="--local-user $(printf '%q' "$SIGNKEY")"
     fi
diff --git a/contrib/scripts/nm-ci-patch-gtkdoc.sh b/contrib/scripts/nm-ci-patch-gtkdoc.sh
deleted file mode 100755
index e72a0a8f..00000000
--- a/contrib/scripts/nm-ci-patch-gtkdoc.sh
+++ /dev/null
@@ -1,40 +0,0 @@
-#!/bin/bash
-
-# patch gtk-doc for https://gitlab.gnome.org/GNOME/gtk-doc/merge_requests/2
-
-cd /
-
-patch -f -p 1 --fuzz 0 --reject-file=- <<EOF
-diff --git a/usr/share/gtk-doc/python/gtkdoc/scan.py b/usr/share/gtk-doc/python/gtkdoc/scan.py
-index f1f167235ab2e4c62676fbcfb87ebbe55c95b944..b59dd17abfa5f42b7bb06d239f9c78e5efffbf5d 100644
---- a/usr/share/gtk-doc/python/gtkdoc/scan.py
-+++ b/usr/share/gtk-doc/python/gtkdoc/scan.py
-@@ -427,20 +427,26 @@ def ScanHeader(input_file, section_list, decl_list, get_types, options):
-             elif m9:
-                 # We've found a 'typedef struct _<name> <name>;'
-                 # This could be an opaque data structure, so we output an
-                 # empty declaration. If the structure is actually found that
-                 # will override this.
-                 structsym = m9.group(1).upper()
-                 logging.info('%s typedef: "%s"', structsym, m9.group(2))
-                 forward_decls[m9.group(2)] = '<%s>\n<NAME>%s</NAME>\n%s</%s>\n' % (
-                     structsym, m9.group(2), deprecated, structsym)
- 
-+                bm = re.search(r'^(\S+)(Class|Iface|Interface)\b', m9.group(2))
-+                if bm:
-+                    objectname = bm.group(1)
-+                    logging.info('Found object: "%s"', objectname)
-+                    title = '<TITLE>%s</TITLE>' % objectname
-+
-             elif re.search(r'^\s*(?:struct|union)\s+_(\w+)\s*;', line):
-                 # Skip private structs/unions.
-                 logging.info('private struct/union')
- 
-             elif m10:
-                 # Do a similar thing for normal structs as for typedefs above.
-                 # But we output the declaration as well in this case, so we
-                 # can differentiate it from a typedef.
-                 structsym = m10.group(1).upper()
-                 logging.info('%s:%s', structsym, m10.group(2))
-EOF
-
diff --git a/meson.build b/meson.build
index e976974e..52bd072b 100644
--- a/meson.build
+++ b/meson.build
@@ -5,7 +5,7 @@ project(
 # NOTE: When incrementing version also add corresponding
 #       NM_VERSION_x_y_z macros in
 #       "src/libnm-core-public/nm-version-macros.h.in"
-  version: '1.51.90',
+  version: '1.52.0',
   license: 'GPL2+',
   default_options: [
     'buildtype=debugoptimized',
diff --git a/src/core/dns/nm-dns-dnsconfd.c b/src/core/dns/nm-dns-dnsconfd.c
index b356c2f9..63b3060f 100644
--- a/src/core/dns/nm-dns-dnsconfd.c
+++ b/src/core/dns/nm-dns-dnsconfd.c
@@ -32,6 +32,8 @@ typedef struct {
     char            *name_owner;
     guint            name_owner_changed_id;
     GCancellable    *name_owner_cancellable;
+    bool             was_start_tried;
+    GCancellable    *start_cancellable;
     GVariant        *latest_update_args;
 
     guint         awaited_configuration_serial;
@@ -61,7 +63,11 @@ G_DEFINE_TYPE(NMDnsDnsconfd, nm_dns_dnsconfd, NM_TYPE_DNS_PLUGIN)
 
 #define DNSCONFD_DBUS_SERVICE "com.redhat.dnsconfd"
 
-typedef enum { CONNECTION_FAIL, CONNECTION_SUCCESS, CONNECTION_WAIT } ConnectionState;
+typedef enum {
+    CONNECTION_FAIL,
+    CONNECTION_SUCCESS,
+    CONNECTION_WAIT,
+} ConnectionState;
 
 /*****************************************************************************/
 
@@ -329,29 +335,8 @@ get_networks(NMDnsConfigIPData *ip_data, char ***networks)
 static void
 server_builder_append_interface_info(GVariantBuilder *argument_builder,
                                      const char      *interface,
-                                     char           **networks,
-                                     const char      *connection_id,
-                                     const char      *connection_uuid,
-                                     const char      *dbus_path)
+                                     char           **networks)
 {
-    if (connection_id) {
-        g_variant_builder_add(argument_builder,
-                              "{sv}",
-                              "connection-id",
-                              g_variant_new("s", connection_id));
-    }
-    if (connection_uuid) {
-        g_variant_builder_add(argument_builder,
-                              "{sv}",
-                              "connection-uuid",
-                              g_variant_new("s", connection_uuid));
-    }
-    if (dbus_path) {
-        g_variant_builder_add(argument_builder,
-                              "{sv}",
-                              "connection-object",
-                              g_variant_new("s", dbus_path));
-    }
     if (interface) {
         g_variant_builder_add(argument_builder, "{sv}", "interface", g_variant_new("s", interface));
     }
@@ -500,6 +485,7 @@ name_owner_changed(NMDnsDnsconfd *self, const char *name_owner)
     }
 
     _LOGT("D-Bus name for dnsconfd got owner %s", name_owner);
+    priv->was_start_tried = FALSE;
 
     if (!subscribe_serial(self)) {
         /* This means that in time between new name and subscribe serial call
@@ -544,6 +530,33 @@ get_name_owner_cb(const char *name_owner, GError *error, gpointer user_data)
     name_owner_changed(user_data, name_owner);
 }
 
+static void
+dnsconfd_start_done(GObject *source_object, GAsyncResult *res, gpointer user_data)
+{
+    NMDnsDnsconfd             *self;
+    NMDnsDnsconfdPrivate      *priv;
+    gs_free_error GError      *error    = NULL;
+    gs_unref_variant GVariant *response = NULL;
+
+    response = g_dbus_connection_call_finish(G_DBUS_CONNECTION(source_object), res, &error);
+    if (nm_utils_error_is_cancelled(error))
+        return;
+
+    self = user_data;
+    priv = NM_DNS_DNSCONFD_GET_PRIVATE(self);
+    nm_clear_g_cancellable(&priv->start_cancellable);
+
+    if (!res) {
+        g_dbus_error_strip_remote_error(error);
+        _LOGW("failed to start Dnsconfd %s", error->message);
+    } else {
+        _LOGT("succesfully started Dnsconfd");
+    }
+
+    /* No update maybe changed or state change, as this is handled by the name owner callbacks
+     * which should be triggered right after this */
+}
+
 static ConnectionState
 ensure_all_connected(NMDnsDnsconfd *self)
 {
@@ -581,6 +594,19 @@ ensure_all_connected(NMDnsDnsconfd *self)
                                                self);
     }
 
+    if (!priv->was_start_tried) {
+        _LOGT("attempting to start Dnsconfd via DBus");
+        priv->was_start_tried = TRUE;
+        nm_clear_g_cancellable(&priv->start_cancellable);
+        priv->start_cancellable = g_cancellable_new();
+        nm_dbus_connection_call_start_service_by_name(priv->dbus_connection,
+                                                      DNSCONFD_DBUS_SERVICE,
+                                                      -1,
+                                                      priv->start_cancellable,
+                                                      dnsconfd_start_done,
+                                                      self);
+    }
+
     return CONNECTION_WAIT;
 }
 
@@ -589,18 +615,11 @@ parse_all_interface_config(GVariantBuilder *argument_builder,
                            const CList     *ip_data_lst_head,
                            const char      *ca)
 {
-    NMDnsConfigIPData    *ip_data;
-    const char *const    *dns_server_strings;
-    guint                 nameserver_count;
-    const char           *ifname;
-    NMDevice             *device;
-    NMActiveConnection   *active_connection;
-    NMSettingsConnection *settings_connection;
-    NMActRequest         *act_request;
-    const char           *connection_id;
-    const char           *connection_uuid;
-    const char           *dbus_path;
-    gboolean              explicit_default = is_default_interface_explicit(ip_data_lst_head);
+    NMDnsConfigIPData *ip_data;
+    const char *const *dns_server_strings;
+    guint              nameserver_count;
+    const char        *ifname;
+    gboolean           explicit_default = is_default_interface_explicit(ip_data_lst_head);
 
     c_list_for_each_entry (ip_data, ip_data_lst_head, ip_data_lst) {
         /* No need to free insides of routing and search domains, as they point to data
@@ -615,23 +634,7 @@ parse_all_interface_config(GVariantBuilder *argument_builder,
                                                                &nameserver_count);
         if (!nameserver_count)
             continue;
-        ifname      = nm_platform_link_get_name(NM_PLATFORM_GET, ip_data->data->ifindex);
-        device      = nm_manager_get_device_by_ifindex(NM_MANAGER_GET, ip_data->data->ifindex);
-        act_request = nm_device_get_act_request(device);
-        active_connection = NM_ACTIVE_CONNECTION(act_request);
-
-        /* Presume that when we have server of this interface then the interface has to have
-         * an active connection */
-        nm_assert(active_connection);
-
-        settings_connection = nm_active_connection_get_settings_connection(active_connection);
-        connection_id       = nm_settings_connection_get_id(settings_connection);
-        connection_uuid     = nm_settings_connection_get_uuid(settings_connection);
-        dbus_path           = nm_dbus_object_get_path_still_exported(NM_DBUS_OBJECT(act_request));
-
-        /* dbus_path also should be set, because if we are parsing this connection then we
-         * expect it to be active and exported on dbus */
-        nm_assert(dbus_path && dbus_path[0] != 0);
+        ifname = nm_platform_link_get_name(NM_PLATFORM_GET, ip_data->data->ifindex);
 
         gather_interface_domains(ip_data, explicit_default, &routing_domains, &search_domains);
         get_networks(ip_data, &networks);
@@ -643,12 +646,7 @@ parse_all_interface_config(GVariantBuilder *argument_builder,
                                            routing_domains,
                                            search_domains,
                                            ca)) {
-                server_builder_append_interface_info(argument_builder,
-                                                     ifname,
-                                                     networks,
-                                                     connection_id,
-                                                     connection_uuid,
-                                                     dbus_path);
+                server_builder_append_interface_info(argument_builder, ifname, networks);
             }
         }
     }
@@ -696,7 +694,10 @@ update(NMDnsPlugin             *plugin,
 
     /* We need to consider only whether we are connected, because newer update call
      * overrides the old one */
-    if (all_connected != CONNECTION_SUCCESS) {
+    if (all_connected == CONNECTION_FAIL) {
+        priv->plugin_state = DNSCONFD_PLUGIN_IDLE;
+        _LOGT("failed to connect");
+    } else if (all_connected == CONNECTION_WAIT) {
         priv->plugin_state = DNSCONFD_PLUGIN_WAIT_CONNECT;
         _LOGT("not connected, waiting to connect");
     } else {
@@ -704,6 +705,8 @@ update(NMDnsPlugin             *plugin,
         _LOGT("connected, waiting for update to finish");
     }
 
+    _nm_dns_plugin_update_pending_maybe_changed(plugin);
+
     if (all_connected == CONNECTION_FAIL) {
         nm_utils_error_set(error,
                            NM_UTILS_ERROR_UNKNOWN,
@@ -717,8 +720,6 @@ update(NMDnsPlugin             *plugin,
 
     send_dnsconfd_update(self);
 
-    _nm_dns_plugin_update_pending_maybe_changed(NM_DNS_PLUGIN(self));
-
     return TRUE;
 }
 
@@ -731,6 +732,7 @@ stop(NMDnsPlugin *plugin)
     nm_clear_g_cancellable(&priv->update_cancellable);
     nm_clear_g_cancellable(&priv->name_owner_cancellable);
     nm_clear_g_cancellable(&priv->serial_cancellable);
+    nm_clear_g_cancellable(&priv->start_cancellable);
     nm_clear_g_dbus_connection_signal(priv->dbus_connection, &priv->name_owner_changed_id);
     nm_clear_g_dbus_connection_signal(priv->dbus_connection, &priv->properties_changed_id);
 }
diff --git a/src/core/nm-l3cfg.c b/src/core/nm-l3cfg.c
index fb48b860..a9aa506d 100644
--- a/src/core/nm-l3cfg.c
+++ b/src/core/nm-l3cfg.c
@@ -372,6 +372,8 @@ G_DEFINE_TYPE(NML3Cfg, nm_l3cfg, G_TYPE_OBJECT)
 #define _NETNS_WATCHER_IP_ADDR_TAG(self, addr_family) \
     ((gconstpointer) & (((char *) self)[1 + NM_IS_IPv4(addr_family)]))
 
+#define _NETNS_WATCHER_MPTCP_IPV6_TAG(self) ((gconstpointer) & (((char *) self)[3]))
+
 /*****************************************************************************/
 
 #define _NMLOG_DOMAIN      LOGD_CORE
@@ -4960,7 +4962,7 @@ next:
     }
 
 out:
-    nm_netns_watcher_remove_all(self->priv.netns, TAG, FALSE);
+    nm_netns_watcher_remove_dirty(self->priv.netns, TAG);
 }
 /*****************************************************************************/
 
@@ -4973,6 +4975,30 @@ _global_tracker_mptcp_untrack(NML3Cfg *self, int addr_family)
                                           TRUE);
 }
 
+static void
+mptcp_ipv6_addr_cb(NMNetns                       *netns,
+                   NMNetnsWatcherType             watcher_type,
+                   const NMNetnsWatcherData      *watcher_data,
+                   gconstpointer                  tag,
+                   const NMNetnsWatcherEventData *event_data,
+                   gpointer                       user_data)
+{
+    NML3Cfg *self = user_data;
+
+    if (event_data->ip_addr.change_type == NM_PLATFORM_SIGNAL_REMOVED)
+        return;
+
+    nm_assert(NMP_OBJECT_GET_TYPE(event_data->ip_addr.obj) == NMP_OBJECT_TYPE_IP6_ADDRESS);
+
+    if (event_data->ip_addr.obj->ip6_address.n_ifa_flags & IFA_F_TENTATIVE)
+        return;
+
+    /* We are inside the handler for a platform event, we should not
+     * perform other operations on platform synchronously. Schedule a
+     * commit in a idle handler. */
+    nm_l3cfg_commit_on_idle_schedule(self, NM_L3_CFG_COMMIT_TYPE_AUTO);
+}
+
 static gboolean
 _l3_commit_mptcp_af(NML3Cfg          *self,
                     NML3CfgCommitType commit_type,
@@ -5051,6 +5077,8 @@ _l3_commit_mptcp_af(NML3Cfg          *self,
                                                         self->priv.p->combined_l3cd_commited,
                                                         addr_family,
                                                         (const NMPlatformIPAddress **) &addr) {
+                const NMPObject *obj;
+
                 /* We want to evaluate the  with-{loopback,link_local}-{4,6} flags based on the actual
                  * ifa_scope that the address will have once we configure it.
                  * "addr" is an address we want to configure, we expect that it will
@@ -5077,6 +5105,34 @@ _l3_commit_mptcp_af(NML3Cfg          *self,
                     break;
                 }
 
+                obj = nm_platform_ip_address_get(self->priv.platform,
+                                                 addr_family,
+                                                 self->priv.ifindex,
+                                                 addr);
+                if (!obj) {
+                    /* The address is not yet configured in platform, typically due to
+                     * IPv4 DAD; skip it for now otherwise the kernel will try to use
+                     * the endpoint, it will fail, and it will never try it again. */
+                    goto skip_addr;
+                }
+                if (!IS_IPv4 && (obj->ip6_address.n_ifa_flags & IFA_F_TENTATIVE)) {
+                    NMNetnsWatcherData watcher_data = {};
+
+                    /* The endpoint is not usable when the address is tentative.
+                     * Watch the address until it becomes non-tentative and then
+                     * schedule a new commit. */
+                    watcher_data.ip_addr.addr.addr_family = AF_INET6;
+                    watcher_data.ip_addr.addr.addr.addr6  = addr->a6.address;
+
+                    nm_netns_watcher_add(self->priv.netns,
+                                         NM_NETNS_WATCHER_TYPE_IP_ADDR,
+                                         &watcher_data,
+                                         _NETNS_WATCHER_MPTCP_IPV6_TAG(self),
+                                         mptcp_ipv6_addr_cb,
+                                         self);
+                    goto skip_addr;
+                }
+
                 a.addr = nm_ip_addr_init(addr_family, addr->ax.address_ptr);
 
                 /* We track the address with different priorities, that depends
@@ -5105,6 +5161,8 @@ skip_addr:
             }
         }
 
+        nm_netns_watcher_remove_dirty(self->priv.netns, _NETNS_WATCHER_MPTCP_IPV6_TAG(self));
+
         if (!any_tracked) {
             /* We need to make it known that this ifindex is used. Track a dummy object. */
             if (nmp_global_tracker_track(
@@ -5841,12 +5899,9 @@ finalize(GObject *object)
     gboolean changed;
 
     if (self->priv.netns) {
-        nm_netns_watcher_remove_all(self->priv.netns,
-                                    _NETNS_WATCHER_IP_ADDR_TAG(self, AF_INET),
-                                    TRUE);
-        nm_netns_watcher_remove_all(self->priv.netns,
-                                    _NETNS_WATCHER_IP_ADDR_TAG(self, AF_INET6),
-                                    TRUE);
+        nm_netns_watcher_remove_all(self->priv.netns, _NETNS_WATCHER_IP_ADDR_TAG(self, AF_INET));
+        nm_netns_watcher_remove_all(self->priv.netns, _NETNS_WATCHER_IP_ADDR_TAG(self, AF_INET6));
+        nm_netns_watcher_remove_all(self->priv.netns, _NETNS_WATCHER_MPTCP_IPV6_TAG(self));
     }
 
     nm_prioq_destroy(&self->priv.p->failedobj_prioq);
diff --git a/src/core/nm-manager.c b/src/core/nm-manager.c
index c9bcbd12..a861b2fe 100644
--- a/src/core/nm-manager.c
+++ b/src/core/nm-manager.c
@@ -2594,7 +2594,7 @@ nm_manager_remove_device(NMManager *self, const char *ifname, NMDeviceType devic
  * Returns: A #NMDevice that was just realized; %NULL if none
  */
 static NMDevice *
-system_create_virtual_device(NMManager *self, NMConnection *connection)
+system_create_virtual_device(NMManager *self, NMConnection *connection, GError **error)
 {
     NMManagerPrivate            *priv = NM_MANAGER_GET_PRIVATE(self);
     NMDeviceFactory             *factory;
@@ -2605,20 +2605,20 @@ system_create_virtual_device(NMManager *self, NMConnection *connection)
     NMDevice                    *device = NULL;
     NMDevice                    *parent = NULL;
     NMDevice                    *dev_candidate;
-    gs_free_error GError        *error = NULL;
-    NMLogLevel                   log_level;
 
     g_return_val_if_fail(NM_IS_MANAGER(self), NULL);
     g_return_val_if_fail(NM_IS_CONNECTION(connection), NULL);
 
-    iface = nm_manager_get_connection_iface(self, connection, &parent, &parent_spec, &error);
-    if (!iface) {
-        _LOG3D(LOGD_DEVICE, connection, "can't get a name of a virtual device: %s", error->message);
+    iface = nm_manager_get_connection_iface(self, connection, &parent, &parent_spec, error);
+    if (!iface)
         return NULL;
-    }
 
     if (parent_spec && !parent) {
-        /* parent is not ready, wait */
+        g_set_error(error,
+                    NM_MANAGER_ERROR,
+                    NM_MANAGER_ERROR_DEPENDENCY_FAILED,
+                    "Parent device for '%s' not available",
+                    iface);
         return NULL;
     }
 
@@ -2626,7 +2626,11 @@ system_create_virtual_device(NMManager *self, NMConnection *connection)
     c_list_for_each_entry (dev_candidate, &priv->devices_lst_head, devices_lst) {
         if (nm_device_check_connection_compatible(dev_candidate, connection, FALSE, NULL)) {
             if (nm_device_is_real(dev_candidate)) {
-                _LOG3D(LOGD_DEVICE, connection, "already created virtual interface name %s", iface);
+                g_set_error(error,
+                            NM_MANAGER_ERROR,
+                            NM_MANAGER_ERROR_UNKNOWN_DEVICE,
+                            "Device named '%s' already exists",
+                            iface);
                 return NULL;
             }
 
@@ -2640,27 +2644,22 @@ system_create_virtual_device(NMManager *self, NMConnection *connection)
 
         factory = nm_device_factory_manager_find_factory_for_connection(connection);
         if (!factory) {
-            _LOG3E(LOGD_DEVICE,
-                   connection,
-                   "(%s) NetworkManager plugin for '%s' unavailable",
-                   iface,
-                   nm_connection_get_connection_type(connection));
+            g_set_error(error,
+                        NM_MANAGER_ERROR,
+                        NM_MANAGER_ERROR_UNKNOWN_DEVICE,
+                        "'%s' plugin for '%s' unavailable",
+                        nm_connection_get_connection_type(connection),
+                        iface);
             return NULL;
         }
 
-        device = nm_device_factory_create_device(factory, iface, NULL, connection, NULL, &error);
-        if (!device) {
-            _LOG3W(LOGD_DEVICE, connection, "factory can't create the device: %s", error->message);
+        device = nm_device_factory_create_device(factory, iface, NULL, connection, NULL, error);
+        if (!device)
             return NULL;
-        }
 
         _LOG3D(LOGD_DEVICE, connection, "create virtual device %s", nm_device_get_iface(device));
 
-        if (!add_device(self, device, &error)) {
-            _LOG3W(LOGD_DEVICE,
-                   connection,
-                   "can't register the device with manager: %s",
-                   error->message);
+        if (!add_device(self, device, error)) {
             g_object_unref(device);
             return NULL;
         }
@@ -2679,10 +2678,8 @@ system_create_virtual_device(NMManager *self, NMConnection *connection)
         return device;
     }
 
-    if (!find_controller(self, connection, device, NULL, NULL, NULL, &error)) {
-        _LOG3D(LOGD_DEVICE, connection, "skip activation: %s", error->message);
+    if (!find_controller(self, connection, device, NULL, NULL, NULL, error))
         return device;
-    }
 
     /* Create backing resources if the device has any autoconnect connections */
     connections = nm_settings_get_connections_sorted_by_autoconnect_priority(priv->settings, NULL);
@@ -2699,18 +2696,8 @@ system_create_virtual_device(NMManager *self, NMConnection *connection)
             continue;
 
         /* Create any backing resources the device needs */
-        if (!nm_device_create_and_realize(device, connection, parent, &error)) {
-            log_level =
-                g_error_matches(error, NM_DEVICE_ERROR, NM_DEVICE_ERROR_MISSING_DEPENDENCIES)
-                    ? LOGL_DEBUG
-                    : LOGL_ERR;
-            _NMLOG3(log_level,
-                    LOGD_DEVICE,
-                    connection,
-                    "couldn't create the device: %s",
-                    error->message);
+        if (!nm_device_create_and_realize(device, connection, parent, error))
             return NULL;
-        }
 
         retry_connections_for_parent_device(self, device);
         break;
@@ -2751,8 +2738,9 @@ retry_connections_for_parent_device(NMManager *self, NMDevice *device)
 static void
 connection_changed(NMManager *self, NMSettingsConnection *sett_conn)
 {
-    NMConnection *connection;
-    NMDevice     *device;
+    NMConnection         *connection;
+    NMDevice             *device;
+    gs_free_error GError *error = NULL;
 
     if (NM_FLAGS_ANY(nm_settings_connection_get_flags(sett_conn),
                      NM_SETTINGS_CONNECTION_INT_FLAGS_VOLATILE
@@ -2764,9 +2752,11 @@ connection_changed(NMManager *self, NMSettingsConnection *sett_conn)
     if (!nm_connection_is_virtual(connection))
         return;
 
-    device = system_create_virtual_device(self, connection);
-    if (!device)
+    device = system_create_virtual_device(self, connection, &error);
+    if (!device) {
+        _LOG3D(LOGD_DEVICE, connection, "Can't create a virtual device: %s", error->message);
         return;
+    }
 
     /* Maybe the device that was created was needed by some other
      * connection's device (parent of a VLAN). Let the connections
@@ -4699,10 +4689,16 @@ found_better:
         if (nm_g_hash_table_contains(exclude_devices, device))
             continue;
 
-        if (!nm_device_is_available(device,
-                                    for_user_request
-                                        ? NM_DEVICE_CHECK_DEV_AVAILABLE_FOR_USER_REQUEST
-                                        : NM_DEVICE_CHECK_DEV_AVAILABLE_NONE))
+        /* During startup, NM performs a cleanup of the ovsdb to remove previous entries.
+         * Before the device is suitable for the connection, it must have ovsdb->ready set
+         * to TRUE. Performing this check in all kind of interfaces is too agressive and leads
+         * to race conditions, e.g when a non-virtual bond port gets a carrier, preventing the
+         * device to be a good candidate for the connection. */
+        if (nm_device_get_device_type(device) == NM_DEVICE_TYPE_OVS_INTERFACE
+            && !nm_device_is_available(device,
+                                       for_user_request
+                                           ? NM_DEVICE_CHECK_DEV_AVAILABLE_FOR_USER_REQUEST
+                                           : NM_DEVICE_CHECK_DEV_AVAILABLE_NONE))
             continue;
 
         /* determine the priority of this device. Currently, this priority is independent
@@ -6542,18 +6538,9 @@ find_device_for_activation(NMManager            *self,
                 return FALSE;
 
             device = find_device_by_iface(self, iface, connection, NULL, NULL);
-            if (!device) {
-                g_set_error_literal(error,
-                                    NM_MANAGER_ERROR,
-                                    NM_MANAGER_ERROR_UNKNOWN_DEVICE,
-                                    "Failed to find a compatible device for this connection");
-                return FALSE;
-            }
         }
     }
 
-    nm_assert(is_vpn || NM_IS_DEVICE(device));
-
     *out_device = device;
     *out_is_vpn = is_vpn;
 
@@ -6678,7 +6665,14 @@ impl_manager_activate_connection(NMDBusObject                      *obj,
     if (!subject)
         goto error;
 
-    if (!find_device_for_activation(self, sett_conn, NULL, device_path, &device, &is_vpn, &error)) {
+    if (!find_device_for_activation(self, sett_conn, NULL, device_path, &device, &is_vpn, &error))
+        goto error;
+
+    if (!device && !is_vpn) {
+        g_set_error_literal(&error,
+                            NM_MANAGER_ERROR,
+                            NM_MANAGER_ERROR_UNKNOWN_DEVICE,
+                            "Failed to find a compatible device for this connection");
         goto error;
     }
 
@@ -6801,6 +6795,7 @@ _add_and_activate_auth_done(NMManager                      *self,
                             const char                     *error_desc)
 {
     NMManagerPrivate *priv;
+    NMDevice         *device;
     GError           *error = NULL;
 
     if (!success) {
@@ -6813,6 +6808,14 @@ _add_and_activate_auth_done(NMManager                      *self,
                                    nm_active_connection_get_subject(active),
                                    error->message);
         g_dbus_method_invocation_take_error(invocation, error);
+
+        device = nm_active_connection_get_device(active);
+        if (device && nm_device_is_software(device)
+            && !nm_device_managed_type_is_external_or_assume(device)
+            && _check_remove_dev_on_link_deleted(self, device)) {
+            remove_device(self, device, FALSE);
+        }
+
         return;
     }
 
@@ -6968,32 +6971,11 @@ impl_manager_add_and_activate_connection(NMDBusObject                      *obj,
         goto error;
     }
 
-    if (is_vpn) {
-        /* Try to fill the VPN's connection setting and name at least */
-        if (!nm_connection_get_setting_vpn(incompl_conn)) {
-            error = g_error_new_literal(NM_CONNECTION_ERROR,
-                                        NM_CONNECTION_ERROR_MISSING_SETTING,
-                                        "VPN connections require a 'vpn' setting");
-            g_prefix_error(&error, "%s: ", NM_SETTING_VPN_SETTING_NAME);
-            goto error;
-        }
-
-        conns = nm_settings_connections_array_to_connections(
-            nm_settings_get_connections(priv->settings, NULL),
-            -1);
+    conns = nm_settings_connections_array_to_connections(
+        nm_settings_get_connections(priv->settings, NULL),
+        -1);
 
-        nm_utils_complete_generic(priv->platform,
-                                  incompl_conn,
-                                  NM_SETTING_VPN_SETTING_NAME,
-                                  conns,
-                                  NULL,
-                                  _("VPN connection"),
-                                  NULL,
-                                  NULL);
-    } else {
-        conns = nm_settings_connections_array_to_connections(
-            nm_settings_get_connections(priv->settings, NULL),
-            -1);
+    if (device) {
         /* Let each device subclass complete the connection */
         if (!nm_device_complete_connection(device,
                                            incompl_conn,
@@ -7001,9 +6983,27 @@ impl_manager_add_and_activate_connection(NMDBusObject                      *obj,
                                            conns,
                                            &error))
             goto error;
-    }
+    } else {
+        nm_utils_complete_generic(priv->platform,
+                                  incompl_conn,
+                                  nm_connection_get_connection_type(incompl_conn),
+                                  conns,
+                                  is_vpn ? _("VPN connection") : NULL,
+                                  nm_connection_get_connection_type(incompl_conn),
+                                  NULL,
+                                  NULL);
+
+        if (!nm_connection_verify(incompl_conn, &error))
+            goto error;
+
+        if (!is_vpn && !device) {
+            nm_assert(nm_connection_is_virtual(incompl_conn));
 
-    nm_assert(_nm_connection_verify(incompl_conn, NULL) == NM_SETTING_VERIFY_SUCCESS);
+            device = system_create_virtual_device(self, incompl_conn, &error);
+            if (!device)
+                goto error;
+        }
+    }
 
     active = _new_active_connection(self,
                                     is_vpn,
diff --git a/src/core/nm-netns.c b/src/core/nm-netns.c
index 420d01e4..57dbe9c5 100644
--- a/src/core/nm-netns.c
+++ b/src/core/nm-netns.c
@@ -1369,8 +1369,8 @@ nm_netns_watcher_remove_handle(NMNetns *self, NMNetnsWatcherHandle *handle)
         g_object_unref(self);
 }
 
-void
-nm_netns_watcher_remove_all(NMNetns *self, gconstpointer tag, gboolean all)
+static void
+watcher_remove(NMNetns *self, gconstpointer tag, gboolean all)
 {
     NMNetnsPrivate       *priv;
     WatcherByTag         *watcher_by_tag;
@@ -1420,6 +1420,21 @@ nm_netns_watcher_remove_all(NMNetns *self, gconstpointer tag, gboolean all)
     }
 }
 
+void
+nm_netns_watcher_remove_all(NMNetns *self, gconstpointer tag)
+{
+    watcher_remove(self, tag, TRUE);
+}
+
+/* Similar to nm_netns_watcher_remove_all(), but removes only watchers
+ * that were marked as "dirty" in a previous call of this function and were
+ * not added back via nm_netns_watcher_add() in the meantime. */
+void
+nm_netns_watcher_remove_dirty(NMNetns *self, gconstpointer tag)
+{
+    watcher_remove(self, tag, FALSE);
+}
+
 /*****************************************************************************/
 
 static void
diff --git a/src/core/nm-netns.h b/src/core/nm-netns.h
index 7725ae79..43e9c781 100644
--- a/src/core/nm-netns.h
+++ b/src/core/nm-netns.h
@@ -98,7 +98,7 @@ void nm_netns_watcher_add(NMNetns                  *self,
                           NMNetnsWatcherCallback    callback,
                           gpointer                  user_data);
 
-void
-nm_netns_watcher_remove_all(NMNetns *self, gconstpointer tag, gboolean all /* or only dirty */);
+void nm_netns_watcher_remove_all(NMNetns *self, gconstpointer tag);
+void nm_netns_watcher_remove_dirty(NMNetns *self, gconstpointer tag);
 
 #endif /* __NM_NETNS_H__ */
diff --git a/src/core/nm-policy.c b/src/core/nm-policy.c
index f86d8115..33073e4a 100644
--- a/src/core/nm-policy.c
+++ b/src/core/nm-policy.c
@@ -1873,8 +1873,7 @@ unblock_autoconnect_for_children(NMPolicy   *self,
                                  const char *parent_device,
                                  const char *parent_uuid_settings,
                                  const char *parent_uuid_applied,
-                                 const char *parent_mac_addr,
-                                 gboolean    reset_devcon_autoconnect)
+                                 const char *parent_mac_addr)
 {
     NMPolicyPrivate             *priv = NM_POLICY_GET_PRIVATE(self);
     NMSettingsConnection *const *connections;
@@ -1915,10 +1914,8 @@ unblock_autoconnect_for_children(NMPolicy   *self,
                           parent_mac_addr))
             continue;
 
-        if (reset_devcon_autoconnect) {
-            if (nm_manager_devcon_autoconnect_retries_reset(priv->manager, NULL, sett_conn))
-                changed = TRUE;
-        }
+        if (nm_manager_devcon_autoconnect_retries_reset(priv->manager, NULL, sett_conn))
+            changed = TRUE;
 
         /* unblock the devices associated with that connection */
         if (nm_manager_devcon_autoconnect_blocked_reason_set(
@@ -1940,12 +1937,11 @@ static void
 unblock_autoconnect_for_ports(NMPolicy   *self,
                               const char *controller_device,
                               const char *controller_uuid_settings,
-                              const char *controller_uuid_applied,
-                              gboolean    reset_devcon_autoconnect)
+                              const char *controller_uuid_applied)
 {
     NMPolicyPrivate             *priv = NM_POLICY_GET_PRIVATE(self);
     NMSettingsConnection *const *connections;
-    gboolean                     changed;
+    gboolean                     changed = FALSE;
     guint                        i;
 
     _LOGT(LOGD_CORE,
@@ -1959,7 +1955,6 @@ unblock_autoconnect_for_ports(NMPolicy   *self,
                               "\"",
                               ""));
 
-    changed     = FALSE;
     connections = nm_settings_get_connections(priv->settings, NULL);
     for (i = 0; connections[i]; i++) {
         NMSettingsConnection *sett_conn = connections[i];
@@ -1977,10 +1972,8 @@ unblock_autoconnect_for_ports(NMPolicy   *self,
                           controller_uuid_settings))
             continue;
 
-        if (reset_devcon_autoconnect) {
-            if (nm_manager_devcon_autoconnect_retries_reset(priv->manager, NULL, sett_conn))
-                changed = TRUE;
-        }
+        if (nm_manager_devcon_autoconnect_retries_reset(priv->manager, NULL, sett_conn))
+            changed = TRUE;
 
         /* unblock the devices associated with that connection */
         if (nm_manager_devcon_autoconnect_blocked_reason_set(
@@ -2015,7 +2008,7 @@ unblock_autoconnect_for_ports_for_sett_conn(NMPolicy *self, NMSettingsConnection
     controller_uuid_settings = nm_setting_connection_get_uuid(s_con);
     controller_device        = nm_setting_connection_get_interface_name(s_con);
 
-    unblock_autoconnect_for_ports(self, controller_device, controller_uuid_settings, NULL, TRUE);
+    unblock_autoconnect_for_ports(self, controller_device, controller_uuid_settings, NULL);
 }
 
 static void
@@ -2028,7 +2021,6 @@ activate_port_or_children_connections(NMPolicy *self,
     const char   *controller_uuid_applied  = NULL;
     const char   *parent_mac_addr          = NULL;
     NMActRequest *req;
-    gboolean      internal_activation = FALSE;
 
     controller_device = nm_device_get_iface(device);
     nm_assert(controller_device);
@@ -2039,7 +2031,6 @@ activate_port_or_children_connections(NMPolicy *self,
     if (req) {
         NMConnection         *connection;
         NMSettingsConnection *sett_conn;
-        NMAuthSubject        *subject;
 
         sett_conn = nm_active_connection_get_settings_connection(NM_ACTIVE_CONNECTION(req));
         if (sett_conn)
@@ -2051,25 +2042,19 @@ activate_port_or_children_connections(NMPolicy *self,
 
         if (nm_streq0(controller_uuid_settings, controller_uuid_applied))
             controller_uuid_applied = NULL;
-
-        subject = nm_active_connection_get_subject(NM_ACTIVE_CONNECTION(req));
-        internal_activation =
-            subject && (nm_auth_subject_get_subject_type(subject) == NM_AUTH_SUBJECT_TYPE_INTERNAL);
     }
 
     if (!activate_children_connections_only) {
         unblock_autoconnect_for_ports(self,
                                       controller_device,
                                       controller_uuid_settings,
-                                      controller_uuid_applied,
-                                      !internal_activation);
+                                      controller_uuid_applied);
     }
     unblock_autoconnect_for_children(self,
                                      controller_device,
                                      controller_uuid_settings,
                                      controller_uuid_applied,
-                                     parent_mac_addr,
-                                     !internal_activation);
+                                     parent_mac_addr);
 }
 
 static gboolean
diff --git a/src/core/settings/plugins/keyfile/nms-keyfile-plugin.c b/src/core/settings/plugins/keyfile/nms-keyfile-plugin.c
index 1679cab6..681eef85 100644
--- a/src/core/settings/plugins/keyfile/nms-keyfile-plugin.c
+++ b/src/core/settings/plugins/keyfile/nms-keyfile-plugin.c
@@ -880,12 +880,11 @@ nms_keyfile_plugin_update_connection(NMSKeyfilePlugin   *self,
                                      NMConnection      **out_connection,
                                      GError            **error)
 {
-    NMSKeyfilePluginPrivate      *priv             = NMS_KEYFILE_PLUGIN_GET_PRIVATE(self);
-    NMSKeyfileStorage            *storage          = NMS_KEYFILE_STORAGE(storage_x);
-    gs_unref_object NMConnection *connection_clone = NULL;
-    gs_unref_object NMConnection *reread           = NULL;
-    gs_free char                 *full_filename    = NULL;
-    gs_free_error GError         *local            = NULL;
+    NMSKeyfilePluginPrivate      *priv          = NMS_KEYFILE_PLUGIN_GET_PRIVATE(self);
+    NMSKeyfileStorage            *storage       = NMS_KEYFILE_STORAGE(storage_x);
+    gs_unref_object NMConnection *reread        = NULL;
+    gs_free char                 *full_filename = NULL;
+    gs_free_error GError         *local         = NULL;
     struct timespec               mtime;
     const char                   *previous_filename;
     gboolean                      reread_same;
@@ -946,7 +945,7 @@ nms_keyfile_plugin_update_connection(NMSKeyfilePlugin   *self,
             &local)) {
         _LOGW("commit: failure to write %s (%s) to \"%s\": %s",
               uuid,
-              nm_connection_get_id(connection_clone),
+              nm_connection_get_id(connection),
               previous_filename,
               local->message);
         g_propagate_error(error, g_steal_pointer(&local));
diff --git a/src/nm-cloud-setup/main.c b/src/nm-cloud-setup/main.c
index 44500ffa..9de93418 100644
--- a/src/nm-cloud-setup/main.c
+++ b/src/nm-cloud-setup/main.c
@@ -140,6 +140,8 @@ out:
 
 /*****************************************************************************/
 
+static const NMUtilsNamedValue *gl_interfaces_map = NULL;
+
 static NMUtilsNamedValue *
 _map_interfaces_parse(void)
 {
@@ -200,46 +202,38 @@ _map_interfaces_parse(void)
 static const char *
 _device_get_hwaddr(NMDevice *device)
 {
-    static const NMUtilsNamedValue *gl_map_interfaces_map = NULL;
-    static gsize                    gl_initialized        = 0;
-    const NMUtilsNamedValue        *map                   = NULL;
+    const NMUtilsNamedValue *map = NULL;
 
     nm_assert(NM_IS_DEVICE_ETHERNET(device) || NM_IS_DEVICE_MACVLAN(device)
               || NM_IS_DEVICE_VLAN(device));
 
-    /* Network interfaces in cloud environments are identified by their permanent
-     * MAC address.
-     *
-     * For testing, we can set NMCS_ENV_NM_CLOUD_SETUP_MAP_INTERFACES
-     * to a ';' separate list of "$INTERFACE=$HWADDR", which means that we
-     * pretend that device with ip-interface "$INTERFACE" has the specified permanent
-     * MAC address. */
-
-    if (g_once_init_enter(&gl_initialized)) {
-        gl_map_interfaces_map = _map_interfaces_parse();
-        g_once_init_leave(&gl_initialized, 1);
-    }
-
-    map = gl_map_interfaces_map;
-    if (G_UNLIKELY(map)) {
-        const char *const iface = nm_device_get_iface(NM_DEVICE(device));
-
-        /* For testing, the device<->hwaddr is remapped and the actual permanent
-         * MAC address of the device ignored. This mapping is configured via
-         * NMCS_ENV_NM_CLOUD_SETUP_MAP_INTERFACES environment variable. */
+    if (NM_IS_DEVICE_ETHERNET(device)) {
+        /* Ethernet interfaces in cloud environments are identified by their permanent
+         * MAC address.
+         *
+         * For testing, we can set NMCS_ENV_NM_CLOUD_SETUP_MAP_INTERFACES
+         * to a ';' separate list of "$INTERFACE=$HWADDR", which means that we
+         * pretend that device with ip-interface "$INTERFACE" has the specified permanent
+         * MAC address. */
+
+        map = gl_interfaces_map;
+        if (G_UNLIKELY(map)) {
+            const char *const iface = nm_device_get_iface(NM_DEVICE(device));
+
+            /* For testing, the device<->hwaddr is remapped and the actual permanent
+             * MAC address of the device ignored. This mapping is configured via
+             * NMCS_ENV_NM_CLOUD_SETUP_MAP_INTERFACES environment variable. */
+            if (!iface)
+                return NULL;
+
+            for (; map->name; map++) {
+                if (nm_streq(map->name, iface))
+                    return map->value_str;
+            }
 
-        if (!iface)
             return NULL;
-
-        for (; map->name; map++) {
-            if (nm_streq(map->name, iface))
-                return map->value_str;
         }
 
-        return NULL;
-    }
-
-    if (NM_IS_DEVICE_ETHERNET(device)) {
         return nm_device_ethernet_get_permanent_hw_address(NM_DEVICE_ETHERNET(device));
     } else {
         return nm_device_get_hw_address(device);
@@ -295,7 +289,7 @@ _nmc_get_ethernet_hwaddrs(NMClient *nmc)
 }
 
 static NMDevice *
-_nmc_get_device_by_hwaddr(NMClient *nmc, const char *hwaddr)
+_nmc_get_device_by_hwaddr(NMClient *nmc, const GType type_device, const char *hwaddr)
 {
     const GPtrArray *devices;
     guint            i;
@@ -307,7 +301,7 @@ _nmc_get_device_by_hwaddr(NMClient *nmc, const char *hwaddr)
         const char   *hwaddr_dev;
         gs_free char *s = NULL;
 
-        if (!NM_IS_DEVICE_ETHERNET(device))
+        if (!G_TYPE_CHECK_INSTANCE_TYPE(device, type_device))
             continue;
 
         hwaddr_dev = _device_get_hwaddr(device);
@@ -427,13 +421,22 @@ _nmc_mangle_connection(NMDevice                             *device,
     NM_SET_OUT(out_skipped_single_addr, FALSE);
     NM_SET_OUT(out_changed, FALSE);
 
+    if (nm_streq(nm_connection_get_connection_type(connection), NM_SETTING_MACVLAN_SETTING_NAME)) {
+        /* The MACVLAN just sits in between, no L3 configuration on it */
+        return;
+    } else if (!nm_streq(nm_connection_get_connection_type(connection),
+                         NM_SETTING_VLAN_SETTING_NAME)) {
+        /* Preserve existing L3 configuration if not a VLAN */
+        if (device) {
+            if ((ac = nm_device_get_active_connection(device))
+                && (remote_connection = NM_CONNECTION(nm_active_connection_get_connection(ac))))
+                remote_s_ip = nm_connection_get_setting_ip4_config(remote_connection);
+        }
+    }
+
     s_ip = nm_connection_get_setting_ip4_config(connection);
     nm_assert(NM_IS_SETTING_IP4_CONFIG(s_ip));
 
-    if ((ac = nm_device_get_active_connection(device))
-        && (remote_connection = NM_CONNECTION(nm_active_connection_get_connection(ac))))
-        remote_s_ip = nm_connection_get_setting_ip4_config(remote_connection);
-
     addrs_new = g_ptr_array_new_full(config_data->ipv4s_len, (GDestroyNotify) nm_ip_address_unref);
     rules_new =
         g_ptr_array_new_full(config_data->ipv4s_len, (GDestroyNotify) nm_ip_routing_rule_unref);
@@ -566,54 +569,31 @@ _nmc_mangle_connection(NMDevice                             *device,
 /*****************************************************************************/
 
 static gboolean
-_config_one(SigTermData                       *sigterm_data,
-            NMClient                          *nmc,
-            const NMCSProviderGetConfigResult *result,
-            guint                              idx)
+_config_existing(SigTermData                          *sigterm_data,
+                 const NMCSProviderGetConfigIfaceData *config_data,
+                 NMClient                             *nmc,
+                 const NMCSProviderGetConfigResult    *result,
+                 const char                           *connection_type,
+                 NMDevice                             *device)
 {
-    const NMCSProviderGetConfigIfaceData *config_data        = result->iface_datas_arr[idx];
-    const char                           *hwaddr             = config_data->hwaddr;
-    gs_unref_object NMDevice             *device             = NULL;
-    gs_unref_object NMConnection         *applied_connection = NULL;
-    guint64                               applied_version_id;
-    gs_free_error GError                 *error = NULL;
-    gboolean                              changed;
-    gboolean                              skipped_single_addr;
-    gboolean                              version_id_changed;
-    guint                                 try_count;
-    gboolean                              any_changes = FALSE;
-    gboolean                              maybe_no_preserved_external_ip;
-
-    g_main_context_iteration(NULL, FALSE);
-
-    if (g_cancellable_is_cancelled(sigterm_data->cancellable))
-        return FALSE;
-
-    device = nm_g_object_ref(_nmc_get_device_by_hwaddr(nmc, hwaddr));
-    if (!device) {
-        _LOGD("config device %s: skip because device not found", hwaddr);
-        return FALSE;
-    }
-
-    if (!nmcs_provider_get_config_iface_data_is_valid(config_data)) {
-        _LOGD("config device %s: skip because meta data not successfully fetched", hwaddr);
-        return FALSE;
-    }
-
-    if (config_data->iface_idx >= 100) {
-        /* since we use the iface_idx to select a table number, the range is limited from
-         * 0 to 99. Note that the providers are required to provide increasing numbers,
-         * so this means we bail out after the first 100 devices.  */
-        _LOGD("config device %s: skip because number of supported interfaces reached", hwaddr);
-        return FALSE;
-    }
+    const char                   *hwaddr             = config_data->hwaddr;
+    gs_unref_object NMConnection *applied_connection = NULL;
+    guint64                       applied_version_id;
+    gs_free_error GError         *error = NULL;
+    gboolean                      changed;
+    gboolean                      skipped_single_addr;
+    gboolean                      version_id_changed;
+    guint                         try_count;
+    gboolean                      any_changes;
+    gboolean                      maybe_no_preserved_external_ip;
 
     _LOGD("config device %s: configuring \"%s\" (%s)...",
           hwaddr,
           nm_device_get_iface(device) ?: "/unknown/",
           nm_object_get_path(NM_OBJECT(device)));
 
-    try_count = 0;
+    try_count   = 0;
+    any_changes = FALSE;
 
 try_again:
     g_clear_object(&applied_connection);
@@ -638,7 +618,7 @@ try_again:
         return any_changes;
     }
 
-    if (_nmc_skip_connection_by_type(applied_connection, NM_SETTING_WIRED_SETTING_NAME)) {
+    if (_nmc_skip_connection_by_type(applied_connection, connection_type)) {
         _LOGD("config device %s: device has no suitable applied connection. Skip", hwaddr);
         return any_changes;
     }
@@ -705,7 +685,7 @@ try_again:
                   nm_connection_get_uuid(applied_connection),
                   error->message);
         }
-        return any_changes;
+        return TRUE;
     }
 
     _LOGD("config device %s: connection \"%s\" (%s) reapplied",
@@ -713,17 +693,254 @@ try_again:
           nm_connection_get_id(applied_connection),
           nm_connection_get_uuid(applied_connection));
 
+    return TRUE;
+}
+
+static gboolean
+_config_ethernet(SigTermData                          *sigterm_data,
+                 const NMCSProviderGetConfigIfaceData *config_data,
+                 NMClient                             *nmc,
+                 const NMCSProviderGetConfigResult    *result)
+{
+    gs_unref_object NMDevice *device = NULL;
+
+    device = nm_g_object_ref(
+        _nmc_get_device_by_hwaddr(nmc, NM_TYPE_DEVICE_ETHERNET, config_data->hwaddr));
+    if (!device) {
+        _LOGD("config device %s: skip because device not found", config_data->hwaddr);
+        return FALSE;
+    }
+
+    return _config_existing(sigterm_data,
+                            config_data,
+                            nmc,
+                            result,
+                            NM_SETTING_WIRED_SETTING_NAME,
+                            device);
+}
+
+static gboolean
+_oci_new_vlan_dev(SigTermData                          *sigterm_data,
+                  const NMCSProviderGetConfigIfaceData *config_data,
+                  NMClient                             *nmc,
+                  const NMCSProviderGetConfigResult    *result,
+                  const char                           *connection_type,
+                  const char                           *parent_hwaddr)
+{
+    const char                         *hwaddr            = config_data->hwaddr;
+    gs_unref_object NMConnection       *connection        = NULL;
+    gs_unref_object NMActiveConnection *active_connection = NULL;
+    gs_free_error GError               *error             = NULL;
+    gs_free char                       *macvlan_name      = NULL;
+    gs_free char                       *connection_id     = NULL;
+    char                               *ifname            = NULL;
+    const char                         *macvlan_parent    = NULL;
+    const char                         *wired_mac_addr    = NULL;
+    const NMUtilsNamedValue            *map               = NULL;
+    const char                         *ip4_config_method;
+    NMSetting                          *s_user;
+
+    connection = nm_simple_connection_new();
+
+    macvlan_name  = g_strdup_printf("macvlan%ld", config_data->iface_idx);
+    connection_id = g_strdup_printf("%s%ld", connection_type, config_data->iface_idx);
+
+    wired_mac_addr = parent_hwaddr;
+    if (nm_streq(connection_type, NM_SETTING_MACVLAN_SETTING_NAME)) {
+        /* In NM-ci, use macvlan.parent instead of wired.mac-address for parent matching
+         * because we are faking the MAC addresses via NM_CLOUD_SETUP_MAP_INTERFACES.
+         * The daemon still needs the real MAC, not the mapped one, so it won't work. */
+        map = gl_interfaces_map;
+        if (G_UNLIKELY(map)) {
+            for (; map->name; map++) {
+                if (nm_streq(map->value_str, parent_hwaddr)) {
+                    macvlan_parent = map->name;
+                    wired_mac_addr = NULL;
+                    break;
+                }
+            }
+        }
+
+        nm_connection_add_setting(connection,
+                                  g_object_new(NM_TYPE_SETTING_MACVLAN,
+                                               NM_SETTING_MACVLAN_MODE,
+                                               NM_SETTING_MACVLAN_MODE_VEPA,
+                                               NM_SETTING_MACVLAN_PARENT,
+                                               macvlan_parent,
+                                               NULL));
+        nm_connection_add_setting(connection,
+                                  g_object_new(NM_TYPE_SETTING_IP6_CONFIG,
+                                               NM_SETTING_IP_CONFIG_METHOD,
+                                               NM_SETTING_IP6_CONFIG_METHOD_DISABLED,
+                                               NULL));
+        ip4_config_method = NM_SETTING_IP4_CONFIG_METHOD_DISABLED;
+        ifname            = macvlan_name;
+    } else if (nm_streq(connection_type, NM_SETTING_VLAN_SETTING_NAME)) {
+        nm_connection_add_setting(connection,
+                                  g_object_new(NM_TYPE_SETTING_VLAN,
+                                               NM_SETTING_VLAN_PARENT,
+                                               macvlan_name,
+                                               NM_SETTING_VLAN_ID,
+                                               config_data->priv.oci.vlan_tag,
+                                               NULL));
+        ip4_config_method = NM_SETTING_IP4_CONFIG_METHOD_MANUAL;
+    } else {
+        g_return_val_if_reached(FALSE);
+    }
+
+    nm_connection_add_setting(connection,
+                              g_object_new(NM_TYPE_SETTING_CONNECTION,
+                                           NM_SETTING_CONNECTION_ID,
+                                           connection_id,
+                                           NM_SETTING_CONNECTION_TYPE,
+                                           connection_type,
+                                           NM_SETTING_CONNECTION_INTERFACE_NAME,
+                                           ifname,
+                                           NULL));
+    nm_connection_add_setting(connection,
+                              g_object_new(NM_TYPE_SETTING_IP4_CONFIG,
+                                           NM_SETTING_IP_CONFIG_METHOD,
+                                           ip4_config_method,
+                                           NULL));
+
+    nm_connection_add_setting(connection,
+                              g_object_new(NM_TYPE_SETTING_WIRED,
+                                           NM_SETTING_WIRED_MAC_ADDRESS,
+                                           wired_mac_addr,
+                                           NM_SETTING_WIRED_CLONED_MAC_ADDRESS,
+                                           hwaddr,
+                                           NULL));
+
+    s_user = nm_setting_user_new();
+    nm_connection_add_setting(connection, s_user);
+    nm_setting_user_set_data(NM_SETTING_USER(s_user),
+                             "org.freedesktop.NetworkManager.origin",
+                             "nm-cloud-setup",
+                             NULL);
+
+    _nmc_mangle_connection(NULL, connection, result, config_data, NULL, NULL);
+
+    _LOGD("config device %s: creating %s connection for VLAN %d on %s...",
+          hwaddr,
+          ifname ?: connection_type,
+          config_data->priv.oci.vlan_tag,
+          parent_hwaddr);
+
+    active_connection = nmcs_add_and_activate(nmc, NULL, connection, &error);
+    if (active_connection == NULL) {
+        if (!nm_utils_error_is_cancelled(error)) {
+            _LOGD("config device %s: failure to activate connection: %s", hwaddr, error->message);
+        }
+        return FALSE;
+    }
+
+    _LOGD("config device %s: connection \"%s\" (%s) created",
+          hwaddr,
+          nm_active_connection_get_id(active_connection),
+          nm_active_connection_get_uuid(active_connection));
+
+    return TRUE;
+}
+
+static gboolean
+_oci_config_vnic_dev(SigTermData                          *sigterm_data,
+                     const NMCSProviderGetConfigIfaceData *config_data,
+                     NMClient                             *nmc,
+                     const NMCSProviderGetConfigResult    *result,
+                     const GType                           device_type,
+                     const char                           *connection_type,
+                     const char                           *parent_hwaddr)
+{
+    gs_unref_object NMDevice *device = NULL;
+
+    device = nm_g_object_ref(_nmc_get_device_by_hwaddr(nmc, device_type, config_data->hwaddr));
+    if (device) {
+        /* There is a device. Modify and reapply the currently applied connection. */
+        return _config_existing(sigterm_data, config_data, nmc, result, connection_type, device);
+    } else {
+        /* There is no device, but we're configuring a VLAN.
+         * We can just go ahead and create one with a new connection. */
+        return _oci_new_vlan_dev(sigterm_data,
+                                 config_data,
+                                 nmc,
+                                 result,
+                                 connection_type,
+                                 parent_hwaddr);
+    }
+}
+
+static gboolean
+_config_one(SigTermData                       *sigterm_data,
+            NMCSProvider                      *provider,
+            NMClient                          *nmc,
+            const NMCSProviderGetConfigResult *result,
+            guint                              idx)
+{
+    const NMCSProviderGetConfigIfaceData *config_data = result->iface_datas_arr[idx];
+    gboolean                              any_changes;
+
+    g_main_context_iteration(NULL, FALSE);
+
+    if (g_cancellable_is_cancelled(sigterm_data->cancellable))
+        return FALSE;
+
+    if (!nmcs_provider_get_config_iface_data_is_valid(config_data)) {
+        _LOGD("config device %s: skip because meta data not successfully fetched",
+              config_data->hwaddr);
+        return FALSE;
+    }
+
+    if (config_data->iface_idx >= 100) {
+        /* since we use the iface_idx to select a table number, the range is limited from
+         * 0 to 99. Note that the providers are required to provide increasing numbers,
+         * so this means we bail out after the first 100 devices.  */
+        _LOGD("config device %s: skip because number of supported interfaces reached",
+              config_data->hwaddr);
+        return FALSE;
+    }
+
+    if (NMCS_IS_PROVIDER_OCI(provider) && config_data->priv.oci.vlan_tag != 0) {
+        if (config_data->priv.oci.parent_hwaddr == NULL) {
+            _LOGW("config device %s: has vlan id %d but no parent device",
+                  config_data->hwaddr,
+                  config_data->priv.oci.vlan_tag);
+            return FALSE;
+        }
+
+        /* MACVLAN first, because VLAN is on top of it. */
+        any_changes = _oci_config_vnic_dev(sigterm_data,
+                                           config_data,
+                                           nmc,
+                                           result,
+                                           NM_TYPE_DEVICE_MACVLAN,
+                                           NM_SETTING_MACVLAN_SETTING_NAME,
+                                           config_data->priv.oci.parent_hwaddr);
+        any_changes += _oci_config_vnic_dev(sigterm_data,
+                                            config_data,
+                                            nmc,
+                                            result,
+                                            NM_TYPE_DEVICE_VLAN,
+                                            NM_SETTING_VLAN_SETTING_NAME,
+                                            config_data->hwaddr);
+
+    } else {
+        any_changes = _config_ethernet(sigterm_data, config_data, nmc, result);
+    }
+
     return any_changes;
 }
 
 static gboolean
-_config_all(SigTermData *sigterm_data, NMClient *nmc, const NMCSProviderGetConfigResult *result)
+_config_all(SigTermData                       *sigterm_data,
+            NMCSProvider                      *provider,
+            NMClient                          *nmc,
+            const NMCSProviderGetConfigResult *result)
 {
     gboolean any_changes = FALSE;
     guint    i;
 
     for (i = 0; i < result->n_iface_datas; i++) {
-        if (_config_one(sigterm_data, nmc, result, i))
+        if (_config_one(sigterm_data, provider, nmc, result, i))
             any_changes = TRUE;
     }
 
@@ -804,11 +1021,14 @@ main(int argc, const char *const *argv)
         goto done;
     }
 
+    /* Initialize map used in test scenarios. */
+    gl_interfaces_map = _map_interfaces_parse();
+
     result = _get_config(sigterm_cancellable, provider, nmc);
     if (!result)
         goto done;
 
-    if (_config_all(&sigterm_data, nmc, result))
+    if (_config_all(&sigterm_data, provider, nmc, result))
         _LOGI("some changes were applied for provider %s", nmcs_provider_get_name(provider));
     else
         _LOGD("no changes were applied for provider %s", nmcs_provider_get_name(provider));
diff --git a/src/nm-cloud-setup/nmcs-provider-oci.c b/src/nm-cloud-setup/nmcs-provider-oci.c
index b0b0edf9..17cd997e 100644
--- a/src/nm-cloud-setup/nmcs-provider-oci.c
+++ b/src/nm-cloud-setup/nmcs-provider-oci.c
@@ -92,6 +92,9 @@ _get_config_done_cb(GObject *source, GAsyncResult *result, gpointer user_data)
     gs_unref_bytes GBytes          *response = NULL;
     gs_free_error GError           *error    = NULL;
     nm_auto_decref_json json_t     *vnics    = NULL;
+    gboolean                        is_baremetal;
+    gs_unref_ptrarray GPtrArray    *phys_nic_macs = NULL;
+    GHashTableIter                  h_iter;
     size_t                          i;
 
     nm_http_client_poll_req_finish(NM_HTTP_CLIENT(source), result, NULL, &response, &error);
@@ -112,12 +115,24 @@ _get_config_done_cb(GObject *source, GAsyncResult *result, gpointer user_data)
         goto out;
     }
 
+    if (json_array_size(vnics) > 0) {
+        is_baremetal = NULL != json_object_get(json_array_get(vnics, 0), "nicIndex");
+        _LOGI("get-config: detected %s instance", is_baremetal ? "baremetal" : "VM");
+    } else {
+        is_baremetal = FALSE;
+        _LOGI("get-config: empty VNICs metadata, cannot detect instance type");
+    }
+
+    if (is_baremetal)
+        phys_nic_macs = g_ptr_array_sized_new(16);
+
     for (i = 0; i < json_array_size(vnics); i++) {
         json_t       *vnic, *field;
         const char   *vnic_id = "", *val;
         gs_free char *mac     = NULL;
         in_addr_t     addr;
         int           prefix;
+        json_int_t    nic_index = -1, vlan_tag = -1;
 
         vnic = json_array_get(vnics, i);
         if (!json_is_object(vnic)) {
@@ -130,12 +145,28 @@ _get_config_done_cb(GObject *source, GAsyncResult *result, gpointer user_data)
 
         field = json_object_get(vnic, "macAddr");
         val   = field && json_is_string(field) ? json_string_value(field) : NULL;
-        if (!val) {
+        mac   = val ? nmcs_utils_hwaddr_normalize(val, json_string_length(field)) : NULL;
+        if (!mac) {
             _VNIC_WARN("missing or invalid 'macAddr', ignoring VNIC");
             continue;
         }
 
-        mac               = nmcs_utils_hwaddr_normalize(val, json_string_length(field));
+        if (is_baremetal) {
+            field     = json_object_get(vnic, "nicIndex");
+            nic_index = field && json_is_integer(field) ? json_integer_value(field) : -1;
+            if (nic_index < 0 || nic_index >= 1024) { /* 1024 = random limit to prevent abuse*/
+                _VNIC_WARN("missing or invalid 'nicIndex', ignoring VNIC");
+                continue;
+            }
+
+            field    = json_object_get(vnic, "vlanTag");
+            vlan_tag = field && json_is_integer(field) ? json_integer_value(field) : -1;
+            if (vlan_tag < 0) {
+                _VNIC_WARN("missing or invalid 'vlanTag', ignoring VNIC");
+                continue;
+            }
+        }
+
         config_iface_data = nmcs_provider_get_config_iface_data_create(get_config_data, FALSE, mac);
         config_iface_data->iface_idx = i;
 
@@ -168,6 +199,48 @@ _get_config_done_cb(GObject *source, GAsyncResult *result, gpointer user_data)
         } else {
             _VNIC_WARN("missing or invalid 'subnetCidrBlock'");
         }
+
+        if (is_baremetal) {
+            gboolean is_phys_nic = vlan_tag == 0;
+
+            /* In baremetal instances, configure VNICs' VLAN (physical NICs don't need it) */
+            if (is_phys_nic) {
+                config_iface_data->priv.oci.vlan_tag      = 0;
+                config_iface_data->priv.oci.parent_hwaddr = NULL;
+                if (nic_index >= phys_nic_macs->len)
+                    g_ptr_array_set_size(phys_nic_macs,
+                                         NM_MAX((guint) (nic_index + 1), phys_nic_macs->len * 2));
+                phys_nic_macs->pdata[nic_index] = (gpointer) config_iface_data->hwaddr;
+            } else {
+                /* We might not have all the physical NICs' MACs yet, save nicIndex for later */
+                config_iface_data->priv.oci.parent_hwaddr = GINT_TO_POINTER((int) nic_index);
+                config_iface_data->priv.oci.vlan_tag      = vlan_tag;
+            }
+        }
+    }
+
+    if (is_baremetal) {
+        g_hash_table_iter_init(&h_iter, get_config_data->result_dict);
+
+        /* Now that all the metadata is processed we should have all the physical NICs' MACs */
+        while (g_hash_table_iter_next(&h_iter, NULL, (gpointer *) &config_iface_data)) {
+            bool is_phys_nic = config_iface_data->priv.oci.vlan_tag == 0;
+            int  nic_index   = GPOINTER_TO_INT(config_iface_data->priv.oci.parent_hwaddr);
+
+            if (is_phys_nic)
+                continue;
+
+            if (nic_index >= phys_nic_macs->len || phys_nic_macs->pdata[nic_index] == NULL) {
+                _LOGW("get-config: physical NIC for nicIndex=%d not found, ignoring VNIC "
+                      "(VNIC macAddr=%s)",
+                      nic_index,
+                      config_iface_data->hwaddr);
+                g_hash_table_iter_remove(&h_iter);
+                continue;
+            }
+
+            config_iface_data->priv.oci.parent_hwaddr = g_strdup(phys_nic_macs->pdata[nic_index]);
+        }
     }
 
 out:
diff --git a/src/nm-cloud-setup/nmcs-provider.c b/src/nm-cloud-setup/nmcs-provider.c
index 0f06c4e2..251749ba 100644
--- a/src/nm-cloud-setup/nmcs-provider.c
+++ b/src/nm-cloud-setup/nmcs-provider.c
@@ -188,6 +188,7 @@ nmcs_provider_get_config_iface_data_create(NMCSProviderGetConfigTaskData *get_co
 
     iface_data  = g_slice_new(NMCSProviderGetConfigIfaceData);
     *iface_data = (NMCSProviderGetConfigIfaceData) {
+        .provider        = g_object_ref(get_config_data->self),
         .get_config_data = get_config_data,
         .hwaddr          = g_strdup(hwaddr),
         .iface_idx       = -1,
@@ -203,6 +204,11 @@ nmcs_provider_get_config_iface_data_create(NMCSProviderGetConfigTaskData *get_co
         iface_data->priv.aliyun = (typeof(iface_data->priv.aliyun)) {
             .has_primary_ip_address = FALSE,
         };
+    } else if (G_OBJECT_TYPE(get_config_data->self) == nmcs_provider_oci_get_type()) {
+        iface_data->priv.oci = (typeof(iface_data->priv.oci)) {
+            .vlan_tag      = 0,
+            .parent_hwaddr = NULL,
+        };
     }
 
     /* the has does not own the key (iface_datta->hwaddr), the lifetime of the
@@ -220,6 +226,9 @@ _iface_data_free(gpointer data)
     g_free(iface_data->ipv4s_arr);
     nm_g_ptr_array_unref(iface_data->iproutes);
     g_free((char *) iface_data->hwaddr);
+    if (G_OBJECT_TYPE(iface_data->provider) == nmcs_provider_oci_get_type())
+        g_free((char *) iface_data->priv.oci.parent_hwaddr);
+    g_clear_object(&iface_data->provider);
 
     nm_g_slice_free(iface_data);
 }
diff --git a/src/nm-cloud-setup/nmcs-provider.h b/src/nm-cloud-setup/nmcs-provider.h
index 98e50ea3..95a591c6 100644
--- a/src/nm-cloud-setup/nmcs-provider.h
+++ b/src/nm-cloud-setup/nmcs-provider.h
@@ -17,6 +17,8 @@ typedef struct {
      * dictionary. */
     const char *hwaddr;
 
+    struct _NMCSProvider *provider;
+
     struct _NMCSProviderGetConfigTaskData *get_config_data;
 
     in_addr_t *ipv4s_arr;
@@ -51,6 +53,10 @@ typedef struct {
             bool      has_primary_ip_address : 1;
             bool      ipv4s_arr_ordered : 1;
         } aliyun;
+        struct {
+            guint32     vlan_tag; /* 0 if no VLAN is needed */
+            const char *parent_hwaddr;
+        } oci;
     } priv;
 
 } NMCSProviderGetConfigIfaceData;
diff --git a/src/nmtui/nmt-page-ip6.c b/src/nmtui/nmt-page-ip6.c
index 7e7a48c5..bd29a3a7 100644
--- a/src/nmtui/nmt-page-ip6.c
+++ b/src/nmtui/nmt-page-ip6.c
@@ -29,6 +29,7 @@ static NmtNewtPopupEntry ip6methods[] = {
     {N_("Automatic (DHCP-only)"), NM_SETTING_IP6_CONFIG_METHOD_DHCP},
     {N_("Link-Local"), NM_SETTING_IP6_CONFIG_METHOD_LINK_LOCAL},
     {N_("Manual"), NM_SETTING_IP6_CONFIG_METHOD_MANUAL},
+    {N_("Shared"), NM_SETTING_IP6_CONFIG_METHOD_SHARED},
     {N_("Disabled"), NM_SETTING_IP6_CONFIG_METHOD_DISABLED},
     {NULL, NULL}};
 
diff --git a/src/tests/client/test-client.py b/src/tests/client/test-client.py
index d01b3bce..ff734d1a 100755
--- a/src/tests/client/test-client.py
+++ b/src/tests/client/test-client.py
@@ -2907,6 +2907,123 @@ class TestNmCloudSetup(unittest.TestCase):
         )
         self.assertEqual(exitstatus, 0, "Unexpectedly returned a non-zero status")
 
+    @cloud_setup_test
+    def test_oci_vlans(self):
+        self._mock_devices()
+
+        oci_meta = "/opc/v2/"
+        self._mock_path(oci_meta + "instance", "{}")
+        self._mock_path(
+            oci_meta + "vnics",
+            """
+        [
+          {
+            "macAddr": "%s",
+            "privateIp": "%s",
+            "subnetCidrBlock": "172.31.16.0/20",
+            "virtualRouterIp": "172.31.16.1",
+            "vlanTag": 0,
+            "nicIndex": 0,
+            "vnicId": "ocid1.vnic.oc1.cz-adamov1.foobarbaz"
+          },
+          {
+            "macAddr": "%s",
+            "privateIp": "%s",
+            "subnetCidrBlock": "172.31.166.0/20",
+            "virtualRouterIp": "172.31.166.1",
+            "vlanTag": 0,
+            "nicIndex": 1,
+            "vnicId": "ocid1.vnic.oc1.uk-hogwarts.expelliarmus"
+          },
+          {
+            "macAddr": "C0:00:00:00:00:10",
+            "privateIp": "172.31.10.10",
+            "subnetCidrBlock": "172.31.10.0/20",
+            "virtualRouterIp": "172.31.10.1",
+            "vlanTag": 700,
+            "nicIndex": 0,
+            "vnicId": "ocid1.vnic.oc1.uk-hogwarts.keka"
+          }
+        ]
+        """
+            % (
+                TestNmCloudSetup._mac1,
+                TestNmCloudSetup._ip1,
+                TestNmCloudSetup._mac2,
+                TestNmCloudSetup._ip2,
+            ),
+        )
+
+        # Run nm-cloud-setup for the first time
+        pexp = self.ctx.cmd_call_pexpect(
+            ENV_NM_TEST_CLIENT_CLOUD_SETUP_PATH,
+            [],
+            {
+                "NM_CLOUD_SETUP_OCI_HOST": self.md_url,
+                "NM_CLOUD_SETUP_LOG": "trace",
+                "NM_CLOUD_SETUP_OCI": "yes",
+            },
+        )
+
+        pexp.expect("provider oci detected")
+        pexp.expect("found interfaces: CC:00:00:00:00:01, CC:00:00:00:00:02")
+        pexp.expect("get-config: starting")
+        pexp.expect("get-config: success")
+        pexp.expect("meta data received")
+
+        # No configuration for the ethernets
+        pexp.expect('configuring "eth0"')
+        pexp.expect("device has no suitable applied connection. Skip")
+
+        # Setting up the VLAN
+        pexp.expect("creating macvlan2 connection for VLAN 700 on CC:00:00:00:00:01...")
+        pexp.expect("creating vlan connection for VLAN 700 on C0:00:00:00:00:10...")
+        pexp.expect("some changes were applied for provider oci")
+
+        (exitstatus, signalstatus, valgrind_log) = self.ctx.cmd_close_pexpect(pexp)
+        Util.valgrind_check_log(valgrind_log, "test_oci_vlans")
+        self.assertIsNone(
+            signalstatus,
+            "Unexpectedly got " + Util.signal_no_to_str(signalstatus or 0),
+        )
+        self.assertEqual(exitstatus, 0, "Unexpectedly returned a non-zero status")
+
+        # TODO: Actually check the contents of the connection
+        # Probably needs changes to the mock service API
+        conn_macvlan = self.ctx.srv.findConnections(con_id="connection-3")
+        assert conn_macvlan is not None
+        conn_vlan = self.ctx.srv.findConnections(con_id="connection-4")
+        assert conn_vlan is not None
+
+        # Run nm-cloud-setup for the second time
+        pexp = self.ctx.cmd_call_pexpect(
+            ENV_NM_TEST_CLIENT_CLOUD_SETUP_PATH,
+            [],
+            {
+                "NM_CLOUD_SETUP_OCI_HOST": self.md_url,
+                "NM_CLOUD_SETUP_LOG": "trace",
+                "NM_CLOUD_SETUP_OCI": "yes",
+            },
+        )
+
+        # Just the same ol' thing, just no changes this time
+        pexp.expect("provider oci detected")
+        pexp.expect("found interfaces: CC:00:00:00:00:01, CC:00:00:00:00:02")
+        pexp.expect("get-config: starting")
+        pexp.expect("get-config: success")
+        pexp.expect("meta data received")
+        pexp.expect('configuring "eth0"')
+        pexp.expect("device has no suitable applied connection. Skip")
+        pexp.expect("no changes were applied for provider oci")
+
+        (exitstatus, signalstatus, valgrind_log) = self.ctx.cmd_close_pexpect(pexp)
+        Util.valgrind_check_log(valgrind_log, "test_oci_vlans")
+        self.assertIsNone(
+            signalstatus,
+            "Unexpectedly got " + Util.signal_no_to_str(signalstatus or 0),
+        )
+        self.assertEqual(exitstatus, 0, "Unexpectedly returned a non-zero status")
+
 
 ###############################################################################