summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/core/devices/nm-device-bond.c85
-rw-r--r--src/core/devices/nm-device-bond.h3
-rw-r--r--src/core/devices/nm-device.c69
-rw-r--r--src/core/devices/wifi/nm-device-wifi.c4
-rw-r--r--src/core/devices/wwan/nm-modem.c1
-rw-r--r--src/core/ndisc/nm-lndp-ndisc.c51
-rw-r--r--src/core/nm-bond-manager.c114
-rw-r--r--src/core/nm-bond-manager.h6
-rw-r--r--src/core/nm-config.c38
-rw-r--r--src/core/nm-firewall-utils.c63
-rw-r--r--src/core/nm-l3cfg.c57
-rw-r--r--src/core/nm-l3cfg.h2
-rw-r--r--src/core/nm-manager.c29
-rw-r--r--src/core/tests/test-core-with-expect.c2
-rw-r--r--src/core/tests/test-core.c66
-rw-r--r--src/libnm-client-aux-extern/nm-libnm-aux.c11
-rw-r--r--src/libnm-client-impl/nm-client.c44
-rw-r--r--src/libnm-client-impl/nm-secret-agent-old.c12
-rw-r--r--src/libnm-core-impl/nm-setting-connection.c10
-rw-r--r--src/libnm-core-impl/nm-setting-infiniband.c2
-rw-r--r--src/libnm-core-impl/nm-setting-ip4-config.c27
-rw-r--r--src/libnm-core-impl/nm-setting-ip6-config.c18
-rw-r--r--src/libnm-core-impl/nm-utils.c4
-rw-r--r--src/libnm-core-public/nm-dbus-interface.h13
-rw-r--r--src/libnm-glib-aux/nm-shared-utils.c11
-rw-r--r--src/libnm-glib-aux/nm-shared-utils.h2
-rw-r--r--src/libnm-platform/nm-linux-platform.c123
-rw-r--r--src/libnm-platform/nm-linux-platform.h3
-rw-r--r--src/libnm-platform/nm-platform.c86
-rw-r--r--src/libnm-platform/nm-platform.h5
-rw-r--r--src/libnm-platform/nmp-base.h10
-rw-r--r--src/libnmc-setting/nm-meta-setting-desc.c2
-rw-r--r--src/nmcli/gen-metadata-nm-settings-nmcli.xml.in4
33 files changed, 819 insertions, 158 deletions
diff --git a/src/core/devices/nm-device-bond.c b/src/core/devices/nm-device-bond.c
index b60dd3f1..3ab17aff 100644
--- a/src/core/devices/nm-device-bond.c
+++ b/src/core/devices/nm-device-bond.c
@@ -923,6 +923,91 @@ deactivate(NMDevice *device)
 
 /*****************************************************************************/
 
+gboolean
+nm_device_bond_is_slb(NMDevice *device)
+{
+    NMConnection  *connection;
+    NMSettingBond *s_bond;
+
+    connection = nm_device_get_applied_connection(device);
+    if (!connection)
+        return FALSE;
+
+    s_bond = nm_connection_get_setting_bond(connection);
+    if (!s_bond)
+        return FALSE;
+
+    if (!_nm_setting_bond_opt_value_as_intbool(s_bond, NM_SETTING_BOND_OPTION_BALANCE_SLB))
+        return FALSE;
+
+    return TRUE;
+}
+
+gboolean
+nm_device_bond_announce_ports_on_slb(NMDevice *controller, NMDevice *port)
+{
+    NMDeviceBond      *self               = NM_DEVICE_BOND(controller);
+    int                port_ifindex       = nm_device_get_ifindex(port);
+    int                controller_ifindex = nm_device_get_ifindex(controller);
+    NML3Cfg           *l3cfg              = nm_device_get_l3cfg(controller);
+    NMDevice          *bond_controller    = nm_device_get_controller(controller);
+    NML3Cfg           *bridge_l3cfg;
+    gs_free in_addr_t *addrs_array = NULL;
+    gsize              addrs_len;
+
+    addrs_array = nm_l3cfg_get_configured_ip4_addresses(l3cfg, &addrs_len);
+
+    if (addrs_len > 0) {
+        /* the bond has IPs configured, it is not attached to a
+         * bridge then. */
+        if (!nm_bond_manager_send_arp(controller_ifindex,
+                                      -1,
+                                      nm_device_get_platform(port),
+                                      addrs_array,
+                                      addrs_len)) {
+            _LOGT(LOGD_BOND,
+                  "failed to send gARP on port %s (ifindex %d)",
+                  nm_device_get_iface(port),
+                  port_ifindex);
+            return FALSE;
+        }
+    } else if (bond_controller
+               && nm_device_get_device_type(bond_controller) == NM_DEVICE_TYPE_BRIDGE) {
+        /* the bond is attached to a bridge, firts let's check if the bridge has IP
+         * configuration. */
+        bridge_l3cfg = nm_device_get_l3cfg(bond_controller);
+        addrs_array  = nm_l3cfg_get_configured_ip4_addresses(bridge_l3cfg, &addrs_len);
+        if (addrs_len > 0) {
+            /* the bridge has IPs configured, announcing them on the bond */
+            if (!nm_bond_manager_send_arp(controller_ifindex,
+                                          -1,
+                                          nm_device_get_platform(port),
+                                          addrs_array,
+                                          addrs_len)) {
+                _LOGT(LOGD_BOND,
+                      "failed to send gARP on port %s (ifindex %d) on behalf of bridge",
+                      nm_device_get_iface(port),
+                      port_ifindex);
+                return FALSE;
+            }
+        }
+
+        /* we are going to ARP probe the content of the FDB table */
+        if (!nm_bond_manager_send_arp(controller_ifindex,
+                                      nm_device_get_ifindex(bond_controller),
+                                      nm_device_get_platform(port),
+                                      NULL,
+                                      0)) {
+            _LOGT(LOGD_BOND, "failed to send ARP probing with content of FDB table");
+            return FALSE;
+        }
+    }
+
+    return TRUE;
+}
+
+/*****************************************************************************/
+
 static void
 nm_device_bond_init(NMDeviceBond *self)
 {
diff --git a/src/core/devices/nm-device-bond.h b/src/core/devices/nm-device-bond.h
index 083189bb..2a415843 100644
--- a/src/core/devices/nm-device-bond.h
+++ b/src/core/devices/nm-device-bond.h
@@ -23,4 +23,7 @@ typedef struct _NMDeviceBondClass NMDeviceBondClass;
 
 GType nm_device_bond_get_type(void);
 
+gboolean nm_device_bond_is_slb(NMDevice *device);
+gboolean nm_device_bond_announce_ports_on_slb(NMDevice *controller, NMDevice *port);
+
 #endif /* NM_DEVICE_BOND_H */
diff --git a/src/core/devices/nm-device.c b/src/core/devices/nm-device.c
index 82c2d6b8..516e13df 100644
--- a/src/core/devices/nm-device.c
+++ b/src/core/devices/nm-device.c
@@ -78,6 +78,7 @@
 #include "nm-hostname-manager.h"
 
 #include "nm-device-generic.h"
+#include "nm-device-bond.h"
 #include "nm-device-bridge.h"
 #include "nm-device-loopback.h"
 #include "nm-device-vlan.h"
@@ -5403,6 +5404,7 @@ get_ip_iface_identifier(NMDevice *self, NMUtilsIPv6IfaceId *out_iid)
     NMDevicePrivate      *priv     = NM_DEVICE_GET_PRIVATE(self);
     NMPlatform           *platform = nm_device_get_platform(self);
     const NMPlatformLink *pllink;
+    NMPLinkAddress        permanent_hwaddr;
     NMLinkType            link_type;
     const guint8         *hwaddr;
     guint8                pseudo_hwaddr[ETH_ALEN];
@@ -5446,6 +5448,21 @@ get_ip_iface_identifier(NMDevice *self, NMUtilsIPv6IfaceId *out_iid)
             hwaddr_len = G_N_ELEMENTS(pseudo_hwaddr);
             link_type  = NM_LINK_TYPE_ETHERNET;
         }
+    } else if (NM_IN_SET(pllink->type,
+                         NM_LINK_TYPE_VTI6,
+                         NM_LINK_TYPE_IP6TNL,
+                         NM_LINK_TYPE_IP6GRE)) {
+        /* Use the "permanent" 48-bit address to construct a EUI64
+         * according to RFC 4291 Appendix A. */
+        if (!nm_platform_link_get_permanent_address(platform, pllink, &permanent_hwaddr))
+            return FALSE;
+        if (permanent_hwaddr.len < ETH_ALEN)
+            return FALSE;
+
+        memcpy(pseudo_hwaddr, permanent_hwaddr.data, ETH_ALEN);
+        hwaddr     = pseudo_hwaddr;
+        hwaddr_len = ETH_ALEN;
+        link_type  = NM_LINK_TYPE_ETHERNET;
     }
 
     success = nm_utils_get_ipv6_interface_identifier(link_type,
@@ -7339,10 +7356,12 @@ device_link_changed(gpointer user_data)
     NMDevicePrivate                *priv              = NM_DEVICE_GET_PRIVATE(self);
     gboolean                        ip_ifname_changed = FALSE;
     nm_auto_nmpobj const NMPObject *pllink_keep_alive = NULL;
+    NMDevice                       *controller;
     const NMPlatformLink           *pllink;
     const char                     *str;
     int                             ifindex;
     gboolean                        was_up;
+    gboolean                        carrier_was_up;
     gboolean                        update_unmanaged_specs = FALSE;
     gboolean                        got_hw_addr            = FALSE, had_hw_addr;
     gboolean                        seen_down              = priv->device_link_changed_down;
@@ -7425,6 +7444,8 @@ device_link_changed(gpointer user_data)
             _LOGD(LOGD_DEVICE, "IPv6 tokenized identifier present on device %s", priv->iface);
     }
 
+    carrier_was_up = priv->carrier;
+
     /* Update carrier from link event if applicable. */
     if (nm_device_has_capability(self, NM_DEVICE_CAP_CARRIER_DETECT)
         && !nm_device_has_capability(self, NM_DEVICE_CAP_NONSTANDARD_CARRIER))
@@ -7441,6 +7462,35 @@ device_link_changed(gpointer user_data)
     was_up   = priv->up;
     priv->up = NM_FLAGS_HAS(pllink->n_ifi_flags, IFF_UP);
 
+    if ((was_up && !priv->up) || (carrier_was_up && !priv->carrier)) {
+        /* the link was up and now is down, or the carrier was up and now is down. We must
+         * check if this is a port of a bond and if that bond is in balance-slb mode to perform
+         * gARP on the controller's port.
+         */
+        controller = nm_device_get_controller(self);
+        if (controller && nm_device_get_device_type(controller) == NM_DEVICE_TYPE_BOND
+            && nm_device_bond_is_slb(controller)) {
+            NMDevicePrivate *controller_priv = NM_DEVICE_GET_PRIVATE(controller);
+            PortInfo        *info;
+
+            _LOGT(
+                LOGD_CORE,
+                "controller %s is a bond in bonding-slb mode, redirecting traffic to another port",
+                nm_device_get_iface(controller));
+
+            c_list_for_each_entry (info, &controller_priv->ports, lst_port) {
+                if (info->port != self && NM_DEVICE_GET_PRIVATE(info->port)->carrier) {
+                    _LOGT(LOGD_CORE,
+                          "sending gARP on port %s (ifindex %d)",
+                          nm_device_get_iface(info->port),
+                          nm_device_get_ifindex(info->port));
+                    if (nm_device_bond_announce_ports_on_slb(controller, info->port))
+                        break;
+                }
+            }
+        }
+    }
+
     if (pllink->initialized && nm_device_get_unmanaged_flags(self, NM_UNMANAGED_PLATFORM_INIT)) {
         nm_device_set_unmanaged_by_user_udev(self);
         nm_device_set_unmanaged_by_user_conf(self);
@@ -9485,6 +9535,7 @@ check_connection_compatible(NMDevice     *self,
     NMSettingMatch       *s_match;
     const GSList         *specs;
     gboolean              has_match = FALSE;
+    NMSettingSriov       *s_sriov   = NULL;
 
     klass = NM_DEVICE_GET_CLASS(self);
     if (klass->connection_type_check_compatible) {
@@ -9502,12 +9553,14 @@ check_connection_compatible(NMDevice     *self,
         return FALSE;
     }
 
-    if (!nm_device_has_capability(self, NM_DEVICE_CAP_SRIOV)
-        && nm_connection_get_setting(connection, NM_TYPE_SETTING_SRIOV)) {
-        nm_utils_error_set_literal(error,
-                                   NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY,
-                                   "device does not support SR-IOV");
-        return FALSE;
+    if (!nm_device_has_capability(self, NM_DEVICE_CAP_SRIOV)) {
+        s_sriov = (NMSettingSriov *) nm_connection_get_setting(connection, NM_TYPE_SETTING_SRIOV);
+        if (s_sriov && nm_setting_sriov_get_total_vfs(s_sriov)) {
+            nm_utils_error_set_literal(error,
+                                       NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY,
+                                       "device does not support SR-IOV");
+            return FALSE;
+        }
     }
 
     conn_iface = nm_manager_get_connection_iface(NM_MANAGER_GET, connection, NULL, NULL, &local);
@@ -10118,7 +10171,7 @@ activate_stage1_device_prepare(NMDevice *self)
             s_sriov = nm_device_get_applied_setting(self, NM_TYPE_SETTING_SRIOV);
         }
 
-        if (s_sriov) {
+        if (s_sriov && nm_device_has_capability(self, NM_DEVICE_CAP_SRIOV)) {
             nm_auto_freev NMPlatformVF **plat_vfs = NULL;
             gs_free_error GError        *error    = NULL;
             NMSriovVF                   *vf;
@@ -10126,8 +10179,6 @@ activate_stage1_device_prepare(NMDevice *self)
             guint                        num;
             guint                        i;
 
-            nm_assert(nm_device_has_capability(self, NM_DEVICE_CAP_SRIOV));
-
             autoprobe = nm_setting_sriov_get_autoprobe_drivers(s_sriov);
             if (autoprobe == NM_TERNARY_DEFAULT) {
                 autoprobe = nm_config_data_get_connection_default_int64(
diff --git a/src/core/devices/wifi/nm-device-wifi.c b/src/core/devices/wifi/nm-device-wifi.c
index f24b9733..ea65499e 100644
--- a/src/core/devices/wifi/nm-device-wifi.c
+++ b/src/core/devices/wifi/nm-device-wifi.c
@@ -327,7 +327,7 @@ _scan_request_ssids_track(NMDeviceWifiPrivate *priv, const GPtrArray *ssids)
         priv->scan_request_ssids_hash = g_hash_table_new(nm_pg_bytes_hash, nm_pg_bytes_equal);
 
     /* Do a little dance. New elements shall keep their order as in @ssids, but all
-     * new elements should be sorted in the list preexisting elements of the list.
+     * new elements should be sorted before preexisting elements of the list.
      * First move the old elements away, and splice them back afterwards. */
     c_list_init(&old_lst_head);
     c_list_splice(&old_lst_head, &priv->scan_request_ssids_lst_head);
@@ -348,6 +348,8 @@ _scan_request_ssids_track(NMDeviceWifiPrivate *priv, const GPtrArray *ssids)
             g_hash_table_add(priv->scan_request_ssids_hash, d);
         } else
             d->timestamp_msec = now_msec;
+
+        c_list_unlink_stale(&d->lst);
         c_list_link_tail(&priv->scan_request_ssids_lst_head, &d->lst);
     }
 
diff --git a/src/core/devices/wwan/nm-modem.c b/src/core/devices/wwan/nm-modem.c
index ea0fa7aa..23e7de4a 100644
--- a/src/core/devices/wwan/nm-modem.c
+++ b/src/core/devices/wwan/nm-modem.c
@@ -206,7 +206,6 @@ nm_modem_emit_signal_new_config(NMModem                  *self,
     nm_assert(NM_IS_MODEM(self));
     nm_assert_addr_family(addr_family);
     nm_assert(!l3cd || NM_IS_L3_CONFIG_DATA(l3cd));
-    nm_assert(!do_auto || addr_family == AF_INET6);
     nm_assert(!iid || addr_family == AF_INET6);
     nm_assert(!error || (!l3cd && !do_auto && !iid));
 
diff --git a/src/core/ndisc/nm-lndp-ndisc.c b/src/core/ndisc/nm-lndp-ndisc.c
index 932366ff..eea79373 100644
--- a/src/core/ndisc/nm-lndp-ndisc.c
+++ b/src/core/ndisc/nm-lndp-ndisc.c
@@ -115,7 +115,8 @@ receive_ra(struct ndp *ndp, struct ndp_msg *msg, gpointer user_data)
     NMNDisc             *ndisc   = (NMNDisc *) user_data;
     NMNDiscDataInternal *rdata   = ndisc->rdata;
     NMNDiscConfigMap     changed = 0;
-    struct ndp_msgra    *msgra   = ndp_msgra(msg);
+    NMNDiscGateway       gateway;
+    struct ndp_msgra    *msgra = ndp_msgra(msg);
     struct in6_addr      gateway_addr;
     const gint64         now_msec = nm_utils_get_monotonic_timestamp_msec();
     int                  offset;
@@ -174,23 +175,17 @@ receive_ra(struct ndp *ndp, struct ndp_msg *msg, gpointer user_data)
      * Subsequent router advertisements can represent new default gateways
      * on the network. We should present all of them in router preference
      * order.
-     */
-    {
-        const NMNDiscGateway gateway = {
-            .address     = gateway_addr,
-            .expiry_msec = _nm_ndisc_lifetime_to_expiry(now_msec, ndp_msgra_router_lifetime(msgra)),
-            .preference  = _route_preference_coerce(ndp_msgra_route_preference(msgra)),
-        };
-
-        /* https://tools.ietf.org/html/rfc2461#section-4.2
-         *   > A Lifetime of 0 indicates that the router is not a
-         *   > default router and SHOULD NOT appear on the default
-         *   > router list.
-         * We handle that by tracking a gateway that expires right now. */
-
-        if (nm_ndisc_add_gateway(ndisc, &gateway, now_msec))
-            changed |= NM_NDISC_CONFIG_GATEWAYS;
-    }
+     *
+     * https://tools.ietf.org/html/rfc2461#section-4.2 :
+     * A Lifetime of 0 indicates that the router is not a default router and
+     * SHOULD NOT appear on the default router list.
+     *
+     * We handle that by tracking a gateway that expires right now. */
+    gateway = (NMNDiscGateway){
+        .address     = gateway_addr,
+        .expiry_msec = _nm_ndisc_lifetime_to_expiry(now_msec, ndp_msgra_router_lifetime(msgra)),
+        .preference  = _route_preference_coerce(ndp_msgra_route_preference(msgra)),
+    };
 
     /* Addresses & Routes */
     ndp_msg_opt_for_each_offset (offset, msg, NDP_MSG_OPT_PREFIX) {
@@ -240,9 +235,24 @@ receive_ra(struct ndp *ndp, struct ndp_msg *msg, gpointer user_data)
         guint8          plen = ndp_msg_opt_route_prefix_len(msg, offset);
         struct in6_addr network;
 
-        if (plen == 0 || plen > 128)
+        if (plen > 128)
             continue;
 
+        if (plen == 0) {
+            /* https://tools.ietf.org/html/rfc4191#section-3.1 :
+             * When processing a Router Advertisement, a type C host first updates a
+             * ::/0 route based on the Router Lifetime and Default Router Preference
+             * in the Router Advertisement message header. [...] The Router Preference
+             * and Lifetime values in a ::/0 Route Information Option override the
+             * preference and lifetime values in the Router Advertisement header.
+             */
+            gateway.preference =
+                _route_preference_coerce(ndp_msg_opt_route_preference(msg, offset));
+            gateway.expiry_msec =
+                _nm_ndisc_lifetime_to_expiry(now_msec, ndp_msg_opt_route_lifetime(msg, offset));
+            continue;
+        }
+
         nm_ip6_addr_clear_host_address(&network, ndp_msg_opt_route_prefix(msg, offset), plen);
 
         {
@@ -262,6 +272,9 @@ receive_ra(struct ndp *ndp, struct ndp_msg *msg, gpointer user_data)
         }
     }
 
+    if (nm_ndisc_add_gateway(ndisc, &gateway, now_msec))
+        changed |= NM_NDISC_CONFIG_GATEWAYS;
+
     ndp_msg_opt_for_each_offset (offset, msg, NDP_MSG_OPT_RDNSS) {
         struct in6_addr *addr;
         int              addr_index;
diff --git a/src/core/nm-bond-manager.c b/src/core/nm-bond-manager.c
index 71cd4ee7..f24c8163 100644
--- a/src/core/nm-bond-manager.c
+++ b/src/core/nm-bond-manager.c
@@ -6,8 +6,13 @@
 
 #include <linux/if.h>
 
+#include <linux/if_ether.h>
+#include <linux/if_packet.h>
+#include <sys/socket.h>
+
 #include "NetworkManagerUtils.h"
 #include "libnm-core-aux-intern/nm-libnm-core-utils.h"
+#include "libnm-platform/nm-linux-platform.h"
 #include "libnm-glib-aux/nm-str-buf.h"
 #include "libnm-platform/nm-platform.h"
 #include "libnm-platform/nmp-object.h"
@@ -94,6 +99,32 @@ struct _NMBondManager {
 
 /*****************************************************************************/
 
+#define IP_ADDR_LEN 4
+
+#define ARP_OP_GARP 0x0001
+#define ARP_OP_RARP 0x0003
+
+#define ARP_HW_TYPE_ETH 0x0001
+
+#define ARP_PROTOCOL_IPV4 0x0800
+
+typedef struct _nm_packed {
+    char    s_addr[ETH_ALEN];
+    char    d_addr[ETH_ALEN];
+    guint16 eth_type;
+    guint16 hw_type;
+    guint16 protocol;
+    guint8  addr_len;
+    guint8  ip_len;
+    guint16 op;
+    char    s_hw_addr[ETH_ALEN];
+    char    s_ip_addr[IP_ADDR_LEN];
+    char    d_hw_addr[ETH_ALEN];
+    char    d_ip_addr[IP_ADDR_LEN];
+} ARPPacket;
+
+/*****************************************************************************/
+
 static void _nft_call(NMBondManager     *self,
                       gboolean           up,
                       const char        *bond_ifname,
@@ -839,6 +870,89 @@ nm_bond_manager_reapply(NMBondManager *self)
     _reconfigure_check(self, TRUE);
 }
 
+gboolean
+nm_bond_manager_send_arp(int                 bond_ifindex,
+                         int                 bridge_ifindex,
+                         struct _NMPlatform *platform,
+                         in_addr_t          *addrs_array,
+                         gsize               addrs_len)
+{
+    struct sockaddr_ll addr = {
+        .sll_family   = AF_PACKET,
+        .sll_protocol = htons(ETH_P_ARP),
+        .sll_ifindex  = bond_ifindex,
+    };
+    ARPPacket         data;
+    const guint8     *hwaddr;
+    gsize             hwaddrlen    = 0;
+    nm_auto_close int sockfd       = -1;
+    bool              announce_fdb = FALSE;
+
+    nm_assert(NM_IS_PLATFORM(platform));
+    nm_assert(bond_ifindex);
+
+    /* if the bridge_ifindex is specified is because we want to
+     * announce the FDB table content from the bridge */
+    if (bridge_ifindex > 0)
+        announce_fdb = TRUE;
+
+    sockfd = socket(PF_PACKET, SOCK_RAW, htons(ETH_P_ARP));
+    if (sockfd < 0)
+        return FALSE;
+
+    hwaddr = nm_platform_link_get_address(platform, bond_ifindex, &hwaddrlen);
+    /* infiniband interfaces not supported */
+    if (hwaddrlen > ETH_ALEN)
+        return FALSE;
+
+    /* common ARP options to be configured */
+    memset(data.d_addr, 0xff, ETH_ALEN);
+    data.eth_type = htons(ETH_P_ARP);
+    data.hw_type  = htons(ARP_HW_TYPE_ETH);
+    data.protocol = htons(ARP_PROTOCOL_IPV4);
+    data.addr_len = ETH_ALEN;
+    data.ip_len   = IP_ADDR_LEN;
+
+    if (announce_fdb) {
+        /* if we are announcing the FDB we do a RARP, we don't set the
+         * source/dest IPv4 address */
+        int                   ifindexes[] = {bridge_ifindex, bond_ifindex};
+        int                   i;
+        gs_free NMEtherAddr **fdb_addrs = NULL;
+
+        fdb_addrs = nm_linux_platform_get_link_fdb_table(platform, ifindexes, 2);
+        /* we want to send a Reverse ARP (RARP) packet */
+        data.op = htons(ARP_OP_RARP);
+
+        i = 0;
+        while (fdb_addrs[i] != NULL) {
+            NMEtherAddr *tmp_hwaddr = fdb_addrs[i];
+            memcpy(data.s_hw_addr, tmp_hwaddr, ETH_ALEN);
+            memcpy(data.d_hw_addr, tmp_hwaddr, ETH_ALEN);
+            memcpy(data.s_addr, tmp_hwaddr, ETH_ALEN);
+            g_free(tmp_hwaddr);
+            if (sendto(sockfd, &data, sizeof(data), 0, (struct sockaddr *) &addr, sizeof(addr)) < 0)
+                return FALSE;
+            i++;
+        }
+    } else {
+        /* we want to send a Gratuitous ARP (GARP) packet */
+        data.op = htons(ARP_OP_GARP);
+        memcpy(data.s_addr, hwaddr, hwaddrlen);
+        memcpy(data.s_hw_addr, hwaddr, hwaddrlen);
+        for (int i = 0; i < addrs_len; i++) {
+            const in_addr_t tmp_addr = addrs_array[i];
+
+            unaligned_write_ne32(data.s_ip_addr, tmp_addr);
+            unaligned_write_ne32(data.d_ip_addr, tmp_addr);
+            if (sendto(sockfd, &data, sizeof(data), 0, (struct sockaddr *) &addr, sizeof(addr)) < 0)
+                return FALSE;
+        }
+    }
+
+    return TRUE;
+}
+
 /*****************************************************************************/
 
 int
diff --git a/src/core/nm-bond-manager.h b/src/core/nm-bond-manager.h
index 92a89f0b..78ada15b 100644
--- a/src/core/nm-bond-manager.h
+++ b/src/core/nm-bond-manager.h
@@ -23,6 +23,12 @@ NMBondManager *nm_bond_manager_new(struct _NMPlatform   *platform,
 
 void nm_bond_manager_reapply(NMBondManager *self);
 
+gboolean nm_bond_manager_send_arp(int                 bond_ifindex,
+                                  int                 bridge_ifindex,
+                                  struct _NMPlatform *platform,
+                                  in_addr_t          *addrs_array,
+                                  gsize               addrs_len);
+
 void nm_bond_manager_destroy(NMBondManager *self);
 
 int          nm_bond_manager_get_ifindex(NMBondManager *self);
diff --git a/src/core/nm-config.c b/src/core/nm-config.c
index 878f343a..cc3d1135 100644
--- a/src/core/nm-config.c
+++ b/src/core/nm-config.c
@@ -1558,6 +1558,7 @@ intern_config_read(const char        *filename,
     gs_strfreev char **groups        = NULL;
     guint              g, k;
     gboolean           has_intern = FALSE;
+    gboolean           has_global_dns;
 
     g_return_val_if_fail(filename, NULL);
 
@@ -1575,6 +1576,8 @@ intern_config_read(const char        *filename,
         goto out;
     }
 
+    has_global_dns = nm_config_keyfile_has_global_dns_config(keyfile_conf, FALSE);
+
     groups = g_key_file_get_groups(keyfile, NULL);
     for (g = 0; groups && groups[g]; g++) {
         gs_strfreev char **keys  = NULL;
@@ -1591,6 +1594,21 @@ intern_config_read(const char        *filename,
         is_intern = NM_STR_HAS_PREFIX(group, NM_CONFIG_KEYFILE_GROUPPREFIX_INTERN);
         is_atomic = !is_intern && _is_atomic_section(atomic_section_prefixes, group);
 
+        if (has_global_dns
+            && (nm_streq0(group, NM_CONFIG_KEYFILE_GROUP_INTERN_GLOBAL_DNS)
+                || NM_STR_HAS_PREFIX_WITH_MORE(
+                    group,
+                    NM_CONFIG_KEYFILE_GROUPPREFIX_INTERN_GLOBAL_DNS_DOMAIN))) {
+            /*
+             * If user configuration specifies global DNS options, the DNS
+             * options in internal configuration must be deleted. Otherwise, a
+             * deletion of options from user configuration may cause the
+             * internal options to appear again.
+             */
+            needs_rewrite = TRUE;
+            continue;
+        }
+
         if (is_atomic) {
             gs_free char *conf_section_was = NULL;
             gs_free char *conf_section_is  = NULL;
@@ -1684,26 +1702,6 @@ intern_config_read(const char        *filename,
     }
 
 out:
-    /*
-     * If user configuration specifies global DNS options, the DNS
-     * options in internal configuration must be deleted. Otherwise, a
-     * deletion of options from user configuration may cause the
-     * internal options to appear again.
-     */
-    if (nm_config_keyfile_has_global_dns_config(keyfile_conf, FALSE)) {
-        if (g_key_file_remove_group(keyfile_intern,
-                                    NM_CONFIG_KEYFILE_GROUP_INTERN_GLOBAL_DNS,
-                                    NULL))
-            needs_rewrite = TRUE;
-        for (g = 0; groups && groups[g]; g++) {
-            if (NM_STR_HAS_PREFIX(groups[g], NM_CONFIG_KEYFILE_GROUPPREFIX_INTERN_GLOBAL_DNS_DOMAIN)
-                && groups[g][NM_STRLEN(NM_CONFIG_KEYFILE_GROUPPREFIX_INTERN_GLOBAL_DNS_DOMAIN)]) {
-                g_key_file_remove_group(keyfile_intern, groups[g], NULL);
-                needs_rewrite = TRUE;
-            }
-        }
-    }
-
     g_key_file_unref(keyfile);
 
     if (out_needs_rewrite)
diff --git a/src/core/nm-firewall-utils.c b/src/core/nm-firewall-utils.c
index 45dab093..a88c6f1a 100644
--- a/src/core/nm-firewall-utils.c
+++ b/src/core/nm-firewall-utils.c
@@ -844,6 +844,18 @@ nm_firewall_nft_stdio_mlag(gboolean           up,
                     chain_name,
                     previous_member);
             _append(&strbuf, "delete chain netdev %s %s", table_name, chain_name);
+
+            chain_name =
+                _strbuf_set_sanitized(&strbuf_1, "tx-redirect-igmp-reports-", previous_member);
+
+            _append(&strbuf,
+                    "add chain netdev %s %s {"
+                    " type filter hook egress device %s priority filter + 1; "
+                    "}",
+                    table_name,
+                    chain_name,
+                    previous_member);
+            _append(&strbuf, "delete chain netdev %s %s", table_name, chain_name);
         }
 
         /* OVS SLB rule 1
@@ -940,6 +952,57 @@ nm_firewall_nft_stdio_mlag(gboolean           up,
                 "add rule netdev %s rx-drop-looped-packets ether saddr @macset-untagged%s drop",
                 table_name,
                 s_counter);
+
+        /* IGMP SNOOPING
+         *
+         * This redirects all IGMP reports to the primary member port. The TOR switches
+         * may prune the multicast tree. If we let the bonding vlan+srcmac hash occur,
+         * then the TOR may send the pruned multicast stream to a bond member port for
+         * which the RX filters will drop (e.g. report out non-primary, stream in
+         * non-primary). If it's known multicast then we must control the pruned tree by
+         * only sending the IGMP reports on a port for which we will accept the traffic,
+         * i.e. the primary member port.
+         */
+        for (i = 0; i < n_active_members; i++) {
+            const char *active_member = active_members[i];
+            const char *chain_name;
+
+            if (!_nft_ifname_valid(active_member))
+                continue;
+
+            chain_name =
+                _strbuf_set_sanitized(&strbuf_1, "tx-redirect-igmp-reports-", active_member);
+
+            _append(&strbuf,
+                    "add chain netdev %s %s {"
+                    " type filter hook egress device %s priority filter + 1; "
+                    "}",
+                    table_name,
+                    chain_name,
+                    active_member);
+            /* first is primary, we clean up in case it was previously a non-primary member */
+            if (i == 0) {
+                _append(&strbuf, "delete chain netdev %s %s", table_name, chain_name);
+                continue;
+            }
+
+            _append(&strbuf,
+                    "add rule netdev %s %s igmp type {"
+                    " membership-report-v1, membership-report-v2, membership-report-v3 "
+                    "}%s fwd to %s",
+                    table_name,
+                    chain_name,
+                    s_counter,
+                    active_members[0]);
+            _append(&strbuf,
+                    "add rule netdev %s %s icmpv6 type {"
+                    " mld-listener-report, mld2-listener-report "
+                    "}%s fwd to %s",
+                    table_name,
+                    chain_name,
+                    s_counter,
+                    active_members[0]);
+        }
     }
 
 out:
diff --git a/src/core/nm-l3cfg.c b/src/core/nm-l3cfg.c
index 2c977991..a7189d3e 100644
--- a/src/core/nm-l3cfg.c
+++ b/src/core/nm-l3cfg.c
@@ -3143,6 +3143,15 @@ handle_start_defending:
              * warning and start a timer to retry. This way (of having a timer pending)
              * we also back off and are rate limited from retrying too frequently. */
             _LOGT_acd(acd_data, "start announcing failed to create probe (%s)", failure_reason);
+
+            if (!nm_platform_link_uses_arp(self->priv.platform, self->priv.ifindex)) {
+                _LOGT_acd(
+                    acd_data,
+                    "give up on ACD and never retry since interface '%s' is configured with NOARP",
+                    nmp_object_link_get_ifname(self->priv.plobj));
+                return;
+            }
+
             _l3_acd_data_timeout_schedule(acd_data, ACD_WAIT_TIME_ANNOUNCE_RESTART_MSEC);
             return;
         }
@@ -4999,7 +5008,7 @@ _l3_commit_one(NML3Cfg              *self,
     }
 
     if (route_table_sync == NM_IP_ROUTE_TABLE_SYNC_MODE_NONE)
-        route_table_sync = NM_IP_ROUTE_TABLE_SYNC_MODE_MAIN;
+        route_table_sync = NM_IP_ROUTE_TABLE_SYNC_MODE_MAIN_AND_NM_ROUTES;
 
     if (any_dirty)
         _obj_states_track_prune_dirty(self, TRUE);
@@ -5028,6 +5037,8 @@ _l3_commit_one(NML3Cfg              *self,
         }
 
         if (c_list_is_empty(&self->priv.p->blocked_lst_head_x[IS_IPv4])) {
+            gs_unref_ptrarray GPtrArray *routes_old = NULL;
+
             addresses_prune =
                 nm_platform_ip_address_get_prune_list(self->priv.platform,
                                                       addr_family,
@@ -5035,10 +5046,28 @@ _l3_commit_one(NML3Cfg              *self,
                                                       nm_g_array_data(ipv6_temp_addrs_keep),
                                                       nm_g_array_len(ipv6_temp_addrs_keep));
 
+            if (route_table_sync == NM_IP_ROUTE_TABLE_SYNC_MODE_MAIN_AND_NM_ROUTES) {
+                GHashTableIter h_iter;
+                ObjStateData  *obj_state;
+
+                /* Get list of all the routes that were configured by us */
+                routes_old = g_ptr_array_new_with_free_func((GDestroyNotify) nmp_object_unref);
+                g_hash_table_iter_init(&h_iter, self->priv.p->obj_state_hash);
+                while (g_hash_table_iter_next(&h_iter, (gpointer *) &obj_state, NULL)) {
+                    if (NMP_OBJECT_GET_TYPE(obj_state->obj) == NMP_OBJECT_TYPE_IP_ROUTE(IS_IPv4)
+                        && obj_state->os_nm_configured)
+                        g_ptr_array_add(routes_old, (gpointer) nmp_object_ref(obj_state->obj));
+                }
+
+                nm_platform_route_objs_sort(routes_old, NM_PLATFORM_IP_ROUTE_CMP_TYPE_SEMANTICALLY);
+            }
+
             routes_prune = nm_platform_ip_route_get_prune_list(self->priv.platform,
                                                                addr_family,
                                                                self->priv.ifindex,
-                                                               route_table_sync);
+                                                               route_table_sync,
+                                                               routes_old);
+
             _obj_state_zombie_lst_prune_all(self, addr_family);
         }
     } else {
@@ -5390,6 +5419,30 @@ nm_l3cfg_get_best_default_route(NML3Cfg *self, int addr_family, gboolean get_com
     return nm_l3_config_data_get_best_default_route(l3cd, addr_family);
 }
 
+in_addr_t *
+nm_l3cfg_get_configured_ip4_addresses(NML3Cfg *self, gsize *out_len)
+{
+    GArray               *array = NULL;
+    NMDedupMultiIter      iter;
+    const NMPObject      *obj;
+    const NML3ConfigData *l3cd;
+
+    l3cd = nm_l3cfg_get_combined_l3cd(self, FALSE);
+
+    if (!l3cd)
+        return NULL;
+
+    array = g_array_new(FALSE, FALSE, sizeof(in_addr_t));
+
+    nm_l3_config_data_iter_obj_for_each (&iter, l3cd, &obj, NMP_OBJECT_TYPE_IP4_ADDRESS) {
+        in_addr_t tmp = NMP_OBJECT_CAST_IP4_ADDRESS(obj)->address;
+        nm_g_array_append_simple(array, tmp);
+    }
+
+    *out_len = array->len;
+    return NM_CAST_ALIGN(in_addr_t, g_array_free(array, FALSE));
+}
+
 /*****************************************************************************/
 
 gboolean
diff --git a/src/core/nm-l3cfg.h b/src/core/nm-l3cfg.h
index f977b10f..4d3537a0 100644
--- a/src/core/nm-l3cfg.h
+++ b/src/core/nm-l3cfg.h
@@ -441,6 +441,8 @@ const NML3ConfigData *nm_l3cfg_get_combined_l3cd(NML3Cfg *self, gboolean get_com
 const NMPObject *
 nm_l3cfg_get_best_default_route(NML3Cfg *self, int addr_family, gboolean get_commited);
 
+in_addr_t *nm_l3cfg_get_configured_ip4_addresses(NML3Cfg *self, gsize *out_len);
+
 /*****************************************************************************/
 
 gboolean nm_l3cfg_has_commited_ip6_addresses_pending_dad(NML3Cfg *self);
diff --git a/src/core/nm-manager.c b/src/core/nm-manager.c
index 0d6c1e2f..b96a9053 100644
--- a/src/core/nm-manager.c
+++ b/src/core/nm-manager.c
@@ -462,21 +462,24 @@ static GVariant *
 _version_info_get(void)
 {
     const guint32 arr[] = {
+        /* The array contains as first element NM_VERSION, which can be
+         * used to numerically compare the version (see also NM_ENCODE_VERSION,
+         * nm_utils_version(), nm_encode_version() and nm_decode_version(). */
         NM_VERSION,
-    };
 
-    /* The array contains as first element NM_VERSION, which can be
-     * used to numerically compare the version (see also NM_ENCODE_VERSION,
-     * nm_utils_version(), nm_encode_version() and nm_decode_version().
-     *
-     * The following elements of the array are a bitfield of capabilities.
-     * These capabilities should only depend on compile-time abilities
-     * (unlike NM_MANAGER_CAPABILITIES, NMCapability). The supported values
-     * are from NMVersionInfoCapability enum. This way to expose capabilities
-     * is more cumbersome but more efficient compared to NM_MANAGER_CAPABILITIES.
-     * As such, it is cheap to add capabilities for something, where you would
-     * avoid it as NM_MANAGER_CAPABILITIES due to the overhead.
-     */
+        /* The following elements of the array are a bitfield of capabilities.
+         * These capabilities should only depend on compile-time abilities
+         * (unlike NM_MANAGER_CAPABILITIES, NMCapability). The supported values
+         * are from NMVersionInfoCapability enum. This way to expose capabilities
+         * is more cumbersome but more efficient compared to NM_MANAGER_CAPABILITIES.
+         * As such, it is cheap to add capabilities for something, where you would
+         * avoid it as NM_MANAGER_CAPABILITIES due to the overhead.
+         *
+         * Each of the array's elements has 32 bits. This means that capabilities
+         * with index 0-31 goes to element #1, with index 32-63 to element #2,
+         * with index 64-95 to element #3 and so on. */
+        1 << NM_VERSION_INFO_CAPABILITY_SYNC_ROUTE_WITH_TABLE,
+    };
 
     return nm_g_variant_new_au(arr, G_N_ELEMENTS(arr));
 }
diff --git a/src/core/tests/test-core-with-expect.c b/src/core/tests/test-core-with-expect.c
index 680843fc..da8659b2 100644
--- a/src/core/tests/test-core-with-expect.c
+++ b/src/core/tests/test-core-with-expect.c
@@ -229,7 +229,7 @@ do_test_nm_utils_kill_child(void)
     char          *argv_watchdog[] = {
         "bash",
         "-c",
-        "sleep 4; "
+        "sleep 15; "
                  "kill -KILL 0; #watchdog for #" TEST_TOKEN,
         NULL,
     };
diff --git a/src/core/tests/test-core.c b/src/core/tests/test-core.c
index c5a598a6..71a3d878 100644
--- a/src/core/tests/test-core.c
+++ b/src/core/tests/test-core.c
@@ -2671,8 +2671,10 @@ test_nm_firewall_nft_stdio_mlag(void)
        "nm-mlag-bond0\012flush table netdev nm-mlag-bond0\012add chain netdev nm-mlag-bond0 "
        "rx-drop-bc-mc-eth2 { type filter hook ingress device eth2 priority filter; }\012delete "
        "chain netdev nm-mlag-bond0 rx-drop-bc-mc-eth2\012add chain netdev nm-mlag-bond0 "
-       "rx-drop-bc-mc-eth1 { type filter hook ingress device eth1 priority filter; }\012delete "
-       "chain netdev nm-mlag-bond0 rx-drop-bc-mc-eth1\012add set netdev nm-mlag-bond0 "
+       "tx-redirect-igmp-reports-eth2 { type filter hook egress device eth2 priority filter + 1; "
+       "}\012delete chain netdev nm-mlag-bond0 tx-redirect-igmp-reports-eth2\012add chain netdev "
+       "nm-mlag-bond0 rx-drop-bc-mc-eth1 { type filter hook ingress device eth1 priority filter; "
+       "}\012delete chain netdev nm-mlag-bond0 rx-drop-bc-mc-eth1\012add set netdev nm-mlag-bond0 "
        "macset-tagged { typeof ether saddr . vlan id; flags dynamic,timeout; }\012add set netdev "
        "nm-mlag-bond0 macset-untagged { typeof ether saddr; flags dynamic,timeout; }\012add chain "
        "netdev nm-mlag-bond0 tx-snoop-source-mac { type filter hook egress device bond0 priority "
@@ -2683,7 +2685,9 @@ test_nm_firewall_nft_stdio_mlag(void)
        "priority filter; }\012add rule netdev nm-mlag-bond0 rx-drop-looped-packets ether saddr . "
        "vlan id @macset-tagged counter drop\012add rule netdev nm-mlag-bond0 "
        "rx-drop-looped-packets ether type vlan counter return\012add rule netdev nm-mlag-bond0 "
-       "rx-drop-looped-packets ether saddr @macset-untagged counter drop\012");
+       "rx-drop-looped-packets ether saddr @macset-untagged counter drop\012add chain netdev "
+       "nm-mlag-bond0 tx-redirect-igmp-reports-eth1 { type filter hook egress device eth1 priority "
+       "filter + 1; }\012delete chain netdev nm-mlag-bond0 tx-redirect-igmp-reports-eth1\012");
 
     _T(TRUE,
        "bond0",
@@ -2695,8 +2699,10 @@ test_nm_firewall_nft_stdio_mlag(void)
        "nm-mlag-bond0\012flush table netdev nm-mlag-bond0\012add chain netdev nm-mlag-bond0 "
        "rx-drop-bc-mc-eth2 { type filter hook ingress device eth2 priority filter; }\012delete "
        "chain netdev nm-mlag-bond0 rx-drop-bc-mc-eth2\012add chain netdev nm-mlag-bond0 "
-       "rx-drop-bc-mc-eth1 { type filter hook ingress device eth1 priority filter; }\012delete "
-       "chain netdev nm-mlag-bond0 rx-drop-bc-mc-eth1\012add set netdev nm-mlag-bond0 "
+       "tx-redirect-igmp-reports-eth2 { type filter hook egress device eth2 priority filter + 1; "
+       "}\012delete chain netdev nm-mlag-bond0 tx-redirect-igmp-reports-eth2\012add chain netdev "
+       "nm-mlag-bond0 rx-drop-bc-mc-eth1 { type filter hook ingress device eth1 priority filter; "
+       "}\012delete chain netdev nm-mlag-bond0 rx-drop-bc-mc-eth1\012add set netdev nm-mlag-bond0 "
        "macset-tagged { typeof ether saddr . vlan id; flags dynamic,timeout; }\012add set netdev "
        "nm-mlag-bond0 macset-untagged { typeof ether saddr; flags dynamic,timeout; }\012add chain "
        "netdev nm-mlag-bond0 tx-snoop-source-mac { type filter hook egress device bond0 priority "
@@ -2707,7 +2713,9 @@ test_nm_firewall_nft_stdio_mlag(void)
        "filter; }\012add rule netdev nm-mlag-bond0 rx-drop-looped-packets ether saddr . vlan id "
        "@macset-tagged drop\012add rule netdev nm-mlag-bond0 rx-drop-looped-packets ether type "
        "vlan return\012add rule netdev nm-mlag-bond0 rx-drop-looped-packets ether saddr "
-       "@macset-untagged drop\012");
+       "@macset-untagged drop\012add chain netdev nm-mlag-bond0 tx-redirect-igmp-reports-eth1 { "
+       "type filter hook egress device eth1 priority filter + 1; }\012delete chain netdev "
+       "nm-mlag-bond0 tx-redirect-igmp-reports-eth1\012");
 
     _T(TRUE,
        "bond0",
@@ -2720,23 +2728,35 @@ test_nm_firewall_nft_stdio_mlag(void)
        "nm-mlag-bond0\012flush table netdev nm-mlag-bond0\012add chain netdev nm-mlag-bond0 "
        "rx-drop-bc-mc-eth4 { type filter hook ingress device eth4 priority filter; }\012delete "
        "chain netdev nm-mlag-bond0 rx-drop-bc-mc-eth4\012add chain netdev nm-mlag-bond0 "
-       "rx-drop-bc-mc-eth5 { type filter hook ingress device eth5 priority filter; }\012delete "
-       "chain netdev nm-mlag-bond0 rx-drop-bc-mc-eth5\012add chain netdev nm-mlag-bond0 "
-       "rx-drop-bc-mc-eth2 { type filter hook ingress device eth2 priority filter; }\012delete "
-       "chain netdev nm-mlag-bond0 rx-drop-bc-mc-eth2\012add chain netdev nm-mlag-bond0 "
-       "rx-drop-bc-mc-eth3 { type filter hook ingress device eth3 priority filter; }\012add rule "
-       "netdev nm-mlag-bond0 rx-drop-bc-mc-eth3 pkttype { broadcast, multicast } drop\012add set "
-       "netdev nm-mlag-bond0 macset-tagged { typeof ether saddr . vlan id; flags dynamic,timeout; "
-       "}\012add set netdev nm-mlag-bond0 macset-untagged { typeof ether saddr; flags "
-       "dynamic,timeout; }\012add chain netdev nm-mlag-bond0 tx-snoop-source-mac { type filter "
-       "hook egress device bond0 priority filter; }\012add rule netdev nm-mlag-bond0 "
-       "tx-snoop-source-mac set update ether saddr . vlan id timeout 5s @macset-tagged "
-       "return\012add rule netdev nm-mlag-bond0 tx-snoop-source-mac set update ether saddr timeout "
-       "5s @macset-untagged\012add chain netdev nm-mlag-bond0 rx-drop-looped-packets { type filter "
-       "hook ingress device bond0 priority filter; }\012add rule netdev nm-mlag-bond0 "
-       "rx-drop-looped-packets ether saddr . vlan id @macset-tagged drop\012add rule netdev "
-       "nm-mlag-bond0 rx-drop-looped-packets ether type vlan return\012add rule netdev "
-       "nm-mlag-bond0 rx-drop-looped-packets ether saddr @macset-untagged drop\012");
+       "tx-redirect-igmp-reports-eth4 { type filter hook egress device eth4 priority filter + 1; "
+       "}\012delete chain netdev nm-mlag-bond0 tx-redirect-igmp-reports-eth4\012add chain netdev "
+       "nm-mlag-bond0 rx-drop-bc-mc-eth5 { type filter hook ingress device eth5 priority filter; "
+       "}\012delete chain netdev nm-mlag-bond0 rx-drop-bc-mc-eth5\012add chain netdev "
+       "nm-mlag-bond0 tx-redirect-igmp-reports-eth5 { type filter hook egress device eth5 priority "
+       "filter + 1; }\012delete chain netdev nm-mlag-bond0 tx-redirect-igmp-reports-eth5\012add "
+       "chain netdev nm-mlag-bond0 rx-drop-bc-mc-eth2 { type filter hook ingress device eth2 "
+       "priority filter; }\012delete chain netdev nm-mlag-bond0 rx-drop-bc-mc-eth2\012add chain "
+       "netdev nm-mlag-bond0 rx-drop-bc-mc-eth3 { type filter hook ingress device eth3 priority "
+       "filter; }\012add rule netdev nm-mlag-bond0 rx-drop-bc-mc-eth3 pkttype { broadcast, "
+       "multicast } drop\012add set netdev nm-mlag-bond0 macset-tagged { typeof ether saddr . vlan "
+       "id; flags dynamic,timeout; }\012add set netdev nm-mlag-bond0 macset-untagged { typeof "
+       "ether saddr; flags dynamic,timeout; }\012add chain netdev nm-mlag-bond0 "
+       "tx-snoop-source-mac { type filter hook egress device bond0 priority filter; }\012add rule "
+       "netdev nm-mlag-bond0 tx-snoop-source-mac set update ether saddr . vlan id timeout 5s "
+       "@macset-tagged return\012add rule netdev nm-mlag-bond0 tx-snoop-source-mac set update "
+       "ether saddr timeout 5s @macset-untagged\012add chain netdev nm-mlag-bond0 "
+       "rx-drop-looped-packets { type filter hook ingress device bond0 priority filter; }\012add "
+       "rule netdev nm-mlag-bond0 rx-drop-looped-packets ether saddr . vlan id @macset-tagged "
+       "drop\012add rule netdev nm-mlag-bond0 rx-drop-looped-packets ether type vlan return\012add "
+       "rule netdev nm-mlag-bond0 rx-drop-looped-packets ether saddr @macset-untagged drop\012add "
+       "chain netdev nm-mlag-bond0 tx-redirect-igmp-reports-eth2 { type filter hook egress device "
+       "eth2 priority filter + 1; }\012delete chain netdev nm-mlag-bond0 "
+       "tx-redirect-igmp-reports-eth2\012add chain netdev nm-mlag-bond0 "
+       "tx-redirect-igmp-reports-eth3 { type filter hook egress device eth3 priority filter + 1; "
+       "}\012add rule netdev nm-mlag-bond0 tx-redirect-igmp-reports-eth3 igmp type { "
+       "membership-report-v1, membership-report-v2, membership-report-v3 } fwd to eth2\012add rule "
+       "netdev nm-mlag-bond0 tx-redirect-igmp-reports-eth3 icmpv6 type { mld-listener-report, "
+       "mld2-listener-report } fwd to eth2\012");
 
     _T(FALSE,
        "bond0",
diff --git a/src/libnm-client-aux-extern/nm-libnm-aux.c b/src/libnm-client-aux-extern/nm-libnm-aux.c
index 5855bc29..77f4a195 100644
--- a/src/libnm-client-aux-extern/nm-libnm-aux.c
+++ b/src/libnm-client-aux-extern/nm-libnm-aux.c
@@ -169,14 +169,11 @@ nmc_client_has_version_info_capability(NMClient *nmc, NMVersionInfoCapability ca
     len--;
     ver++;
 
-    idx = (gsize) capability;
-    if (idx >= G_MAXSIZE - 31u)
-        return FALSE;
-
-    idx_hi = ((idx + 31u) / 32u);
-    idx_lo = (idx % 32u);
+    idx    = (gsize) capability;
+    idx_hi = idx / 32u;
+    idx_lo = idx % 32u;
 
-    if (idx_hi > len)
+    if (idx_hi >= len)
         return FALSE;
 
     return NM_FLAGS_ANY(ver[idx_hi], (1ull << idx_lo));
diff --git a/src/libnm-client-impl/nm-client.c b/src/libnm-client-impl/nm-client.c
index 4ecc8389..6e722c5c 100644
--- a/src/libnm-client-impl/nm-client.c
+++ b/src/libnm-client-impl/nm-client.c
@@ -4773,8 +4773,8 @@ nm_client_save_hostname(NMClient     *client,
  * @hostname: (nullable): the new persistent hostname to set, or %NULL to
  *   clear any existing persistent hostname
  * @cancellable: a #GCancellable, or %NULL
- * @callback: (scope async): callback to be called when the operation completes
- * @user_data: (closure): caller-specific data passed to @callback
+ * @callback: (scope async) (closure user_data): callback to be called when the operation completes
+ * @user_data: caller-specific data passed to @callback
  *
  * Requests that the machine's persistent hostname be set to the specified value
  * or cleared.
@@ -5771,8 +5771,8 @@ _add_connection_call(NMClient                     *self,
  *   added, not the object itself
  * @save_to_disk: whether to immediately save the connection to disk
  * @cancellable: a #GCancellable, or %NULL
- * @callback: (scope async): callback to be called when the add operation completes
- * @user_data: (closure): caller-specific data passed to @callback
+ * @callback: (scope async) (closure user_data): callback to be called when the add operation completes
+ * @user_data: caller-specific data passed to @callback
  *
  * Requests that the remote settings service add the given settings to a new
  * connection.  If @save_to_disk is %TRUE, the connection is immediately written
@@ -5844,8 +5844,8 @@ nm_client_add_connection_finish(NMClient *client, GAsyncResult *result, GError *
  *   not yet provide AddConnection2(). By setting this to %FALSE, the function
  *   under the hood always calls AddConnection2().
  * @cancellable: a #GCancellable, or %NULL
- * @callback: (scope async): callback to be called when the add operation completes
- * @user_data: (closure): caller-specific data passed to @callback
+ * @callback: (scope async) (closure user_data): callback to be called when the add operation completes
+ * @user_data: caller-specific data passed to @callback
  *
  * Call AddConnection2() D-Bus API asynchronously.
  *
@@ -5971,8 +5971,8 @@ nm_client_load_connections(NMClient     *client,
  * @client: the %NMClient
  * @filenames: (array zero-terminated=1): %NULL-terminated array of filenames to load
  * @cancellable: a #GCancellable, or %NULL
- * @callback: (scope async): callback to be called when the operation completes
- * @user_data: (closure): caller-specific data passed to @callback
+ * @callback: (scope async) (closure user_data): callback to be called when the operation completes
+ * @user_data: caller-specific data passed to @callback
  *
  * Requests that the remote settings service asynchronously load or reload the
  * given files, adding or updating the connections described within.
@@ -6086,8 +6086,8 @@ nm_client_reload_connections(NMClient *client, GCancellable *cancellable, GError
  * nm_client_reload_connections_async:
  * @client: the #NMClient
  * @cancellable: a #GCancellable, or %NULL
- * @callback: (scope async): callback to be called when the reload operation completes
- * @user_data: (closure): caller-specific data passed to @callback
+ * @callback: (scope async) (closure user_data): callback to be called when the reload operation completes
+ * @user_data: caller-specific data passed to @callback
  *
  * Requests that the remote settings service begin reloading all connection
  * files from disk, adding, updating, and removing connections until the
@@ -6315,7 +6315,7 @@ nm_client_get_capabilities(NMClient *client, gsize *length)
  *
  * If available, the first element in the array is NM_VERSION which
  * encodes the daemon version as "(major << 16 | minor << 8 | micro)".
- * The following elements are a bitfield of %NMVersionInfoCapabilities
+ * The following elements are a bitfield of %NMVersionInfoCapability
  * that indicate that the daemon supports a certain capability.
  *
  * Returns: (transfer none) (array length=length): the
@@ -6443,8 +6443,8 @@ checkpoint_create_cb(GObject *object, GAsyncResult *result, gpointer user_data)
  * @rollback_timeout: the rollback timeout in seconds
  * @flags: creation flags
  * @cancellable: a #GCancellable, or %NULL
- * @callback: (scope async): callback to be called when the add operation completes
- * @user_data: (closure): caller-specific data passed to @callback
+ * @callback: (scope async) (closure user_data): callback to be called when the add operation completes
+ * @user_data: caller-specific data passed to @callback
  *
  * Creates a checkpoint of the current networking configuration
  * for given interfaces. An empty @devices argument means all
@@ -6516,8 +6516,8 @@ nm_client_checkpoint_create_finish(NMClient *client, GAsyncResult *result, GErro
  * @client: the %NMClient
  * @checkpoint_path: the D-Bus path for the checkpoint
  * @cancellable: a #GCancellable, or %NULL
- * @callback: (scope async): callback to be called when the add operation completes
- * @user_data: (closure): caller-specific data passed to @callback
+ * @callback: (scope async) (closure user_data): callback to be called when the add operation completes
+ * @user_data: caller-specific data passed to @callback
  *
  * Destroys an existing checkpoint without performing a rollback.
  *
@@ -6576,8 +6576,8 @@ nm_client_checkpoint_destroy_finish(NMClient *client, GAsyncResult *result, GErr
  * @client: the %NMClient
  * @checkpoint_path: the D-Bus path to the checkpoint
  * @cancellable: a #GCancellable, or %NULL
- * @callback: (scope async): callback to be called when the add operation completes
- * @user_data: (closure): caller-specific data passed to @callback
+ * @callback: (scope async) (closure user_data): callback to be called when the add operation completes
+ * @user_data: caller-specific data passed to @callback
  *
  * Performs the rollback of a checkpoint before the timeout is reached.
  *
@@ -6658,8 +6658,8 @@ nm_client_checkpoint_rollback_finish(NMClient *client, GAsyncResult *result, GEr
  * @add_timeout: the timeout in seconds counting from now.
  *   Set to zero, to disable the timeout.
  * @cancellable: a #GCancellable, or %NULL
- * @callback: (scope async): callback to be called when the add operation completes
- * @user_data: (closure): caller-specific data passed to @callback
+ * @callback: (scope async) (closure user_data): callback to be called when the add operation completes
+ * @user_data: caller-specific data passed to @callback
  *
  * Resets the timeout for the checkpoint with path @checkpoint_path
  * to @timeout_add.
@@ -6723,8 +6723,8 @@ nm_client_checkpoint_adjust_rollback_timeout_finish(NMClient     *client,
  * @client: the %NMClient
  * @flags: flags indicating what to reload.
  * @cancellable: a #GCancellable, or %NULL
- * @callback: (scope async): callback to be called when the add operation completes
- * @user_data: (closure): caller-specific data passed to @callback
+ * @callback: (scope async) (closure user_data): callback to be called when the add operation completes
+ * @user_data: caller-specific data passed to @callback
  *
  * Reload NetworkManager's configuration and perform certain updates, like
  * flushing caches or rewriting external state to disk. This is similar to
@@ -8312,7 +8312,7 @@ nm_client_class_init(NMClientClass *client_class)
      * Expose version info and capabilities of NetworkManager. If non-empty,
      * the first element is NM_VERSION, which encodes the version of the
      * daemon as "(major << 16 | minor << 8 | micro)". The following elements
-     * is a bitfields of %NMVersionInfoCapabilities. If a bit is set, then
+     * is a bitfields of %NMVersionInfoCapability. If a bit is set, then
      * the running NetworkManager has the respective capability.
      *
      * Since: 1.42
diff --git a/src/libnm-client-impl/nm-secret-agent-old.c b/src/libnm-client-impl/nm-secret-agent-old.c
index 93b6048f..8f7e0b4d 100644
--- a/src/libnm-client-impl/nm-secret-agent-old.c
+++ b/src/libnm-client-impl/nm-secret-agent-old.c
@@ -970,8 +970,8 @@ nm_secret_agent_old_unregister_finish(NMSecretAgentOld *self, GAsyncResult *resu
  * @setting_name: the name of the secret setting
  * @hints: (array zero-terminated=1): hints to the agent
  * @flags: flags that modify the behavior of the request
- * @callback: (scope async): a callback, to be invoked when the operation is done
- * @user_data: (closure): caller-specific data to be passed to @callback
+ * @callback: (scope async) (closure user_data): a callback, to be invoked when the operation is done
+ * @user_data: caller-specific data to be passed to @callback
  *
  * Asynchronously retrieves secrets belonging to @connection for the
  * setting @setting_name.  @flags indicate specific behavior that the secret
@@ -1010,8 +1010,8 @@ nm_secret_agent_old_get_secrets(NMSecretAgentOld              *self,
  * nm_secret_agent_old_save_secrets: (virtual save_secrets)
  * @self: a #NMSecretAgentOld
  * @connection: a #NMConnection
- * @callback: (scope async): a callback, to be invoked when the operation is done
- * @user_data: (closure): caller-specific data to be passed to @callback
+ * @callback: (scope async) (closure user_data): a callback, to be invoked when the operation is done
+ * @user_data: caller-specific data to be passed to @callback
  *
  * Asynchronously ensures that all secrets inside @connection are stored to
  * disk.
@@ -1037,8 +1037,8 @@ nm_secret_agent_old_save_secrets(NMSecretAgentOld               *self,
  * nm_secret_agent_old_delete_secrets: (virtual delete_secrets)
  * @self: a #NMSecretAgentOld
  * @connection: a #NMConnection
- * @callback: (scope async): a callback, to be invoked when the operation is done
- * @user_data: (closure): caller-specific data to be passed to @callback
+ * @callback: (scope async) (closure user_data): a callback, to be invoked when the operation is done
+ * @user_data: caller-specific data to be passed to @callback
  *
  * Asynchronously asks the agent to delete all saved secrets belonging to
  * @connection.
diff --git a/src/libnm-core-impl/nm-setting-connection.c b/src/libnm-core-impl/nm-setting-connection.c
index 3298dce6..33d088bb 100644
--- a/src/libnm-core-impl/nm-setting-connection.c
+++ b/src/libnm-core-impl/nm-setting-connection.c
@@ -2154,7 +2154,15 @@ nm_setting_connection_class_init(NMSettingConnectionClass *klass)
      * property: uuid
      * variable: UUID(+)
      * description: UUID for the connection profile. When missing, NetworkManager
-     *   creates the UUID itself (by hashing the filename).
+     *   creates the UUID by hashing the connection filename.
+     * ---end---
+     */
+    /* ---keyfile---
+     * property: uuid
+     * variable: uuid
+     * description: UUID for the connection profile. When missing, NetworkManager
+     *   creates the UUID by hashing the connection filename.
+     * example: uuid=7bdbe39a-126c-3f94-ac5e-8f156ed38383
      * ---end---
      */
     _nm_setting_property_define_direct_string(properties_override,
diff --git a/src/libnm-core-impl/nm-setting-infiniband.c b/src/libnm-core-impl/nm-setting-infiniband.c
index 203e4b2b..24e5807a 100644
--- a/src/libnm-core-impl/nm-setting-infiniband.c
+++ b/src/libnm-core-impl/nm-setting-infiniband.c
@@ -346,7 +346,7 @@ nm_setting_infiniband_class_init(NMSettingInfinibandClass *klass)
      * property: mac-address
      * format: usual hex-digits-and-colons notation
      * description: MAC address in traditional hex-digits-and-colons notation, or
-     *   or semicolon separated list of 20 decimal bytes (obsolete)
+     *   semicolon separated list of 20 decimal bytes (obsolete)
      * example: mac-address= 80:00:00:6d:fe:80:00:00:00:00:00:00:00:02:55:00:70:33:cf:01
      * ---end---
      */
diff --git a/src/libnm-core-impl/nm-setting-ip4-config.c b/src/libnm-core-impl/nm-setting-ip4-config.c
index c79d0fdf..6112137f 100644
--- a/src/libnm-core-impl/nm-setting-ip4-config.c
+++ b/src/libnm-core-impl/nm-setting-ip4-config.c
@@ -636,6 +636,7 @@ nm_setting_ip4_config_class_init(NMSettingIP4ConfigClass *klass)
 
     /* ---keyfile---
      * property: dns
+     * variable: dns
      * format: list of DNS IP addresses
      * description: List of DNS servers.
      * example: dns=1.2.3.4;8.8.8.8;8.8.4.4;
@@ -662,9 +663,15 @@ nm_setting_ip4_config_class_init(NMSettingIP4ConfigClass *klass)
     /* ---keyfile---
      * property: addresses
      * variable: address1, address2, ...
-     * format: address/plen
-     * description: List of static IP addresses.
-     * example: address1=192.168.100.100/24 address2=10.1.1.5/24
+     * format: address/prefix-length[,gateway]
+     * description: Static IPv4 addresses, one address per variable. The
+     *   variables can also contain the gateway after a comma or semicolon;
+     *   it is recommended to use the "gateway" variable instead.
+     * example: address1=192.168.100.100/24
+     *
+     *          address2=10.1.1.5/16
+     *
+     *          address1=192.168.100.100/24,192.168.100.1
      * ---end---
      */
     /* ---ifcfg-rh---
@@ -679,8 +686,12 @@ nm_setting_ip4_config_class_init(NMSettingIP4ConfigClass *klass)
      * property: gateway
      * variable: gateway
      * format: string
-     * description: Gateway IP addresses as a string.
-     * example: gateway=192.168.100.1
+     * description: Gateway IP address as a string. The gateway can be also specified in one
+     *   of the "address1", "address2", etc. variables after the address, separated by a comma or
+     *   semicolon (for example "address1=192.168.100.1/24,192.168.100.254").
+     *   The value from the "gateway" variable takes precedence over any gateway specified in one
+     *   of the "address*" variables.
+     * example: gateway=192.168.100.254
      * ---end---
      */
     /* ---ifcfg-rh---
@@ -722,7 +733,8 @@ nm_setting_ip4_config_class_init(NMSettingIP4ConfigClass *klass)
      * variable: routing-rule1, routing-rule2, ...
      * format: routing rule string
      * description: Routing rules as defined with `ip rule add`, but with mandatory
-     *    fixed priority.
+     *    fixed priority. The "lookup" and "table" options don't support a table name,
+     *    only a number.
      * example: routing-rule1=priority 5 from 192.167.4.0/24 table 45
      * ---end---
      */
@@ -1274,7 +1286,8 @@ nm_setting_ip4_config_class_init(NMSettingIP4ConfigClass *klass)
      *   A comma separated list of routing rules for policy routing. The format
      *   is based on <command>ip rule add</command> syntax and mostly compatible.
      *   One difference is that routing rules in NetworkManager always need a
-     *   fixed priority.
+     *   fixed priority. Also, the "lookup" and "table" options don't support a
+     *   table name, only a number.
      *   </para>
      *   <para>
      *   Example: <literal>priority 5 from 192.167.4.0/24 table 45</literal>
diff --git a/src/libnm-core-impl/nm-setting-ip6-config.c b/src/libnm-core-impl/nm-setting-ip6-config.c
index eddac9f1..32fb295f 100644
--- a/src/libnm-core-impl/nm-setting-ip6-config.c
+++ b/src/libnm-core-impl/nm-setting-ip6-config.c
@@ -745,9 +745,15 @@ nm_setting_ip6_config_class_init(NMSettingIP6ConfigClass *klass)
     /* ---keyfile---
      * property: addresses
      * variable: address1, address2, ...
-     * format: address/plen
-     * description: List of static IP addresses.
-     * example: address1=abbe::cafe/96 address2=2001::1234
+     * format: address/plen[,gateway]
+     * description: Static IPv6 addresses, one address per variable. The
+     *   variables can also contain the gateway after a comma or semicolon;
+     *   it is recommended to use the "gateway" variable instead.
+     * example: address1=abbe::cafe/96
+     *
+     *          address2=2001::1234/56
+     *
+     *          address1=fd01::2000/64;fd01::1
      * ---end---
      */
     /* ---ifcfg-rh---
@@ -763,7 +769,11 @@ nm_setting_ip6_config_class_init(NMSettingIP6ConfigClass *klass)
      * property: gateway
      * variable: gateway
      * format: string
-     * description: Gateway IP addresses as a string.
+     * description: Gateway IP address as a string. The gateway can be also specified in one
+     *   of the "address1", "address2", etc. variables after the address, separated by a comma or
+     *   semicolon (for example "address1=fd01::2000/64;fd01::1").
+     *   The value from the "gateway" variable takes precedence over any gateway specified in one
+     *   of the "address*" variables.
      * example: gateway=abbe::1
      * ---end---
      */
diff --git a/src/libnm-core-impl/nm-utils.c b/src/libnm-core-impl/nm-utils.c
index f7f4e770..fea13a95 100644
--- a/src/libnm-core-impl/nm-utils.c
+++ b/src/libnm-core-impl/nm-utils.c
@@ -3626,10 +3626,10 @@ _nm_utils_check_module_file(const char               *name,
  *   Can be empty or %NULL, in which case only @try_first is checked.
  * @file_test_flags: the flags passed to g_file_test() when searching
  *   for @progname. Set it to 0 to skip the g_file_test().
- * @predicate: (scope call): if given, pass the file name to this function
+ * @predicate: (scope call) (closure user_data): if given, pass the file name to this function
  *   for additional checks. This check is performed after the check for
  *   @file_test_flags. You cannot omit both @file_test_flags and @predicate.
- * @user_data: (closure) (nullable): user data for @predicate function.
+ * @user_data: (nullable): user data for @predicate function.
  * @error: on failure, set a "not found" error %G_IO_ERROR %G_IO_ERROR_NOT_FOUND.
  *
  * Searches for a @progname file in a list of search @paths.
diff --git a/src/libnm-core-public/nm-dbus-interface.h b/src/libnm-core-public/nm-dbus-interface.h
index 5eedd7da..9c737dbe 100644
--- a/src/libnm-core-public/nm-dbus-interface.h
+++ b/src/libnm-core-public/nm-dbus-interface.h
@@ -93,16 +93,19 @@
 
 /**
  * NMVersionInfoCapability:
- * %_NM_VERSION_INFO_CAPABILITY_UNUSED: a dummy capability. It has no meaning,
- *   don't use it.
+ * @NM_VERSION_INFO_CAPABILITY_SYNC_ROUTE_WITH_TABLE: Contains the fix to a bug that
+ *   caused that routes in table other than main were not removed on reapply nor
+ *   on connection down.
+ *   https://issues.redhat.com/browse/RHEL-66262
+ *   https://issues.redhat.com/browse/RHEL-67324
  *
- * Currently no enum values are defined. These capabilities are exposed
- * on D-Bus in the "VersionInfo" bit field.
+ * The numeric values represent the bit index of the capability. These capabilities
+ * can be queried in the "VersionInfo" D-Bus property.
  *
  * Since: 1.42
  */
 typedef enum {
-    _NM_VERSION_INFO_CAPABILITY_UNUSED = 0x7FFFFFFFu,
+    NM_VERSION_INFO_CAPABILITY_SYNC_ROUTE_WITH_TABLE = 0,
 } NMVersionInfoCapability;
 
 /**
diff --git a/src/libnm-glib-aux/nm-shared-utils.c b/src/libnm-glib-aux/nm-shared-utils.c
index 421e4d1b..25c78fd3 100644
--- a/src/libnm-glib-aux/nm-shared-utils.c
+++ b/src/libnm-glib-aux/nm-shared-utils.c
@@ -58,6 +58,17 @@ nm_ether_addr_from_string(NMEtherAddr *addr, const char *str)
     return addr;
 }
 
+guint
+nm_ether_addr_hash(const NMEtherAddr *a)
+{
+    NMHashState h;
+
+    nm_hash_init(&h, 1947951703u);
+    nm_hash_update(&h, a, sizeof(NMEtherAddr));
+
+    return nm_hash_complete(&h);
+}
+
 /*****************************************************************************/
 
 /**
diff --git a/src/libnm-glib-aux/nm-shared-utils.h b/src/libnm-glib-aux/nm-shared-utils.h
index ca9feb60..70f1912e 100644
--- a/src/libnm-glib-aux/nm-shared-utils.h
+++ b/src/libnm-glib-aux/nm-shared-utils.h
@@ -240,6 +240,8 @@ nm_ether_addr_is_zero(const NMEtherAddr *a)
     return nm_memeq(a, &nm_ether_addr_zero, sizeof(NMEtherAddr));
 }
 
+guint nm_ether_addr_hash(const NMEtherAddr *a);
+
 /*****************************************************************************/
 
 struct ether_addr;
diff --git a/src/libnm-platform/nm-linux-platform.c b/src/libnm-platform/nm-linux-platform.c
index bd495fe2..90a34102 100644
--- a/src/libnm-platform/nm-linux-platform.c
+++ b/src/libnm-platform/nm-linux-platform.c
@@ -323,6 +323,10 @@ G_STATIC_ASSERT(RTA_MAX == (__RTA_MAX - 1));
 #define IFLA_VF_VLAN_INFO_UNSPEC 0
 #define IFLA_VF_VLAN_INFO        1
 
+/*****************************************************************************/
+
+#define NDA_CONTROLLER NDA_MASTER
+
 /* valid for TRUST, SPOOFCHK, LINK_STATE, RSS_QUERY_EN */
 struct _ifla_vf_setting {
     guint32 vf;
@@ -10352,6 +10356,125 @@ link_get_driver_info(NMPlatform *platform,
 
 /*****************************************************************************/
 
+typedef struct {
+    int         ifindexes_len;
+    int        *ifindexes;
+    GHashTable *out_fdb_addrs;
+} FdbData;
+
+static int
+parse_fdb_cb(const struct nl_msg *msg, void *arg)
+{
+    struct nlmsghdr *nlh          = nlmsg_hdr(msg);
+    struct ndmsg    *ndmsg        = NLMSG_DATA(nlh);
+    int              from_ifindex = ndmsg->ndm_ifindex;
+    bool             match        = FALSE;
+
+    static const struct nla_policy policy[] = {
+        [NDA_LLADDR]     = {.minlen = ETH_ALEN, .maxlen = ETH_ALEN},
+        [NDA_CONTROLLER] = {.type = NLA_U32},
+    };
+    struct nlattr *tb[G_N_ELEMENTS(policy)];
+    FdbData       *data           = arg;
+    int            fdb_controller = -1;
+
+    if (nlmsg_parse_arr(nlh, sizeof(*ndmsg), tb, policy) < 0)
+        return NL_SKIP;
+
+    if (tb[NDA_CONTROLLER])
+        fdb_controller = nla_get_u32(tb[NDA_CONTROLLER]);
+
+    for (int i = 0; i < data->ifindexes_len; i++) {
+        int current_ifindex = data->ifindexes[i];
+
+        if (NM_IN_SET(current_ifindex, from_ifindex, fdb_controller)) {
+            match = TRUE;
+            break;
+        }
+    }
+
+    if (!match)
+        return NL_SKIP;
+
+    if (tb[NDA_LLADDR]) {
+        NMEtherAddr *hwaddr = g_new(NMEtherAddr, 1);
+        memcpy(hwaddr, nla_data(tb[NDA_LLADDR]), ETH_ALEN);
+        g_hash_table_add(data->out_fdb_addrs, hwaddr);
+    }
+
+    return NL_OK;
+}
+
+NMEtherAddr **
+nm_linux_platform_get_link_fdb_table(NMPlatform *platform, int *ifindexes, guint ifindexes_len)
+{
+    int                            nle;
+    struct nl_sock                *sk        = NULL;
+    nm_auto_nlmsg struct nl_msg   *msg       = NULL;
+    gs_unref_hashtable GHashTable *fdb_addrs = NULL;
+    FdbData                        data;
+    const struct ndmsg             ndm = {
+                    .ndm_family = AF_BRIDGE,
+    };
+    gpointer *ret = NULL;
+
+    nm_assert(ifindexes);
+    nm_assert(ifindexes_len >= 1);
+
+    fdb_addrs = g_hash_table_new_full((GHashFunc) nm_ether_addr_hash,
+                                      (GEqualFunc) nm_ether_addr_equal,
+                                      g_free,
+                                      NULL);
+
+    msg = nlmsg_alloc_new(0, RTM_GETNEIGH, NLM_F_REQUEST | NLM_F_DUMP);
+
+    if (nlmsg_append_struct(msg, &ndm) < 0)
+        goto err;
+
+    nle = nl_socket_new(&sk, NETLINK_ROUTE, NL_SOCKET_FLAGS_DISABLE_MSG_PEEK, 0, 0);
+    if (nle < 0) {
+        _LOGD("get-link-fdb: error opening socket: %s (%d)", nm_strerror(nle), nle);
+        goto err;
+    }
+
+    nle = nl_send_auto(sk, msg);
+    if (nle < 0) {
+        _LOGD("get-link-fdb: failed sending request: %s (%d)", nm_strerror(nle), nle);
+        goto err;
+    }
+
+    data = ((FdbData) {
+        .ifindexes_len = ifindexes_len,
+        .ifindexes     = ifindexes,
+        .out_fdb_addrs = fdb_addrs,
+    });
+
+    do {
+        nle = nl_recvmsgs(sk,
+                          &((const struct nl_cb) {
+                              .valid_cb  = parse_fdb_cb,
+                              .valid_arg = &data,
+                          }));
+    } while (nle == -EAGAIN);
+
+    if (nle < 0) {
+        _LOGD("get-link-fdb: recv failed: %s (%d)", nm_strerror(nle), nle);
+        goto err;
+    }
+
+    ret = g_hash_table_get_keys_as_array(fdb_addrs, NULL);
+    g_hash_table_steal_all(fdb_addrs);
+    nl_socket_free(sk);
+    return NM_CAST_ALIGN(NMEtherAddr *, ret);
+
+err:
+    if (sk)
+        nl_socket_free(sk);
+    return NULL;
+}
+
+/*****************************************************************************/
+
 static gboolean
 ip4_address_add(NMPlatform *platform,
                 int         ifindex,
diff --git a/src/libnm-platform/nm-linux-platform.h b/src/libnm-platform/nm-linux-platform.h
index 08135a4a..3f591b7f 100644
--- a/src/libnm-platform/nm-linux-platform.h
+++ b/src/libnm-platform/nm-linux-platform.h
@@ -25,6 +25,9 @@ GType nm_linux_platform_get_type(void);
 
 struct _NMDedupMultiIndex;
 
+NMEtherAddr **
+nm_linux_platform_get_link_fdb_table(NMPlatform *platform, int *ifindexes, guint ifindexes_len);
+
 NMPlatform *nm_linux_platform_new(struct _NMDedupMultiIndex *multi_idx,
                                   gboolean                   log_with_ptr,
                                   gboolean                   netns_support,
diff --git a/src/libnm-platform/nm-platform.c b/src/libnm-platform/nm-platform.c
index af04f29f..658efadb 100644
--- a/src/libnm-platform/nm-platform.c
+++ b/src/libnm-platform/nm-platform.c
@@ -61,6 +61,8 @@ G_STATIC_ASSERT(sizeof(((NMPlatformLink *) NULL)->l_address.data) == _NM_UTILS_H
 G_STATIC_ASSERT(sizeof(((NMPlatformLink *) NULL)->l_perm_address.data) == _NM_UTILS_HWADDR_LEN_MAX);
 G_STATIC_ASSERT(sizeof(((NMPlatformLink *) NULL)->l_broadcast.data) == _NM_UTILS_HWADDR_LEN_MAX);
 
+static int _route_objs_cmp_values(gconstpointer a, gconstpointer b, gpointer user_data);
+
 static const char *
 _nmp_link_port_data_to_string(NMPortKind                    port_kind,
                               const NMPlatformLinkPortData *port_data,
@@ -4872,11 +4874,24 @@ nm_platform_ip_address_get_prune_list(NMPlatform            *self,
     return result;
 }
 
+static gboolean
+_route_obj_find_bsearch(GPtrArray *sorted_routes_objs, const NMPObject *route_obj)
+{
+    gssize pos =
+        nm_ptrarray_find_bsearch((gconstpointer *) sorted_routes_objs->pdata,
+                                 sorted_routes_objs->len,
+                                 route_obj,
+                                 _route_objs_cmp_values,
+                                 GINT_TO_POINTER((int) NM_PLATFORM_IP_ROUTE_CMP_TYPE_SEMANTICALLY));
+    return pos >= 0;
+}
+
 GPtrArray *
 nm_platform_ip_route_get_prune_list(NMPlatform            *self,
                                     int                    addr_family,
                                     int                    ifindex,
-                                    NMIPRouteTableSyncMode route_table_sync)
+                                    NMIPRouteTableSyncMode route_table_sync,
+                                    GPtrArray             *sorted_old_routes_objs)
 {
     NMPLookup                    lookup;
     GPtrArray                   *routes_prune = NULL;
@@ -4890,10 +4905,21 @@ nm_platform_ip_route_get_prune_list(NMPlatform            *self,
     nm_assert(NM_IN_SET(addr_family, AF_INET, AF_INET6));
     nm_assert(NM_IN_SET(route_table_sync,
                         NM_IP_ROUTE_TABLE_SYNC_MODE_MAIN,
-                        NM_IP_ROUTE_TABLE_SYNC_MODE_FULL,
+                        NM_IP_ROUTE_TABLE_SYNC_MODE_ALL_EXCEPT_LOCAL,
+                        NM_IP_ROUTE_TABLE_SYNC_MODE_MAIN_AND_NM_ROUTES,
                         NM_IP_ROUTE_TABLE_SYNC_MODE_ALL,
                         NM_IP_ROUTE_TABLE_SYNC_MODE_ALL_PRUNE));
 
+    if (route_table_sync == NM_IP_ROUTE_TABLE_SYNC_MODE_MAIN_AND_NM_ROUTES) {
+        nm_assert(sorted_old_routes_objs);
+        nm_assert(nm_utils_ptrarray_is_sorted(
+            (gconstpointer *) sorted_old_routes_objs->pdata,
+            sorted_old_routes_objs->len,
+            FALSE,
+            _route_objs_cmp_values,
+            GINT_TO_POINTER((int) NM_PLATFORM_IP_ROUTE_CMP_TYPE_SEMANTICALLY)));
+    }
+
     nmp_lookup_init_object_by_ifindex(&lookup,
                                       NMP_OBJECT_TYPE_IP_ROUTE(NM_IS_IPv4(addr_family)),
                                       ifindex);
@@ -4915,7 +4941,12 @@ nm_platform_ip_route_get_prune_list(NMPlatform            *self,
             if (!nm_platform_route_table_is_main(nm_platform_ip_route_get_effective_table(&rt->rx)))
                 continue;
             break;
-        case NM_IP_ROUTE_TABLE_SYNC_MODE_FULL:
+        case NM_IP_ROUTE_TABLE_SYNC_MODE_MAIN_AND_NM_ROUTES:
+            if (!nm_platform_route_table_is_main(nm_platform_ip_route_get_effective_table(&rt->rx))
+                && !_route_obj_find_bsearch(sorted_old_routes_objs, obj))
+                continue;
+            break;
+        case NM_IP_ROUTE_TABLE_SYNC_MODE_ALL_EXCEPT_LOCAL:
             if (nm_platform_ip_route_get_effective_table(&rt->rx) == RT_TABLE_LOCAL)
                 continue;
             break;
@@ -5284,7 +5315,8 @@ nm_platform_ip_route_flush(NMPlatform *self, int addr_family, int ifindex)
         routes_prune = nm_platform_ip_route_get_prune_list(self,
                                                            AF_INET,
                                                            ifindex,
-                                                           NM_IP_ROUTE_TABLE_SYNC_MODE_ALL_PRUNE);
+                                                           NM_IP_ROUTE_TABLE_SYNC_MODE_ALL_PRUNE,
+                                                           NULL);
         success &= nm_platform_ip_route_sync(self, AF_INET, ifindex, NULL, routes_prune, NULL);
     }
     if (NM_IN_SET(addr_family, AF_UNSPEC, AF_INET6)) {
@@ -5293,7 +5325,8 @@ nm_platform_ip_route_flush(NMPlatform *self, int addr_family, int ifindex)
         routes_prune = nm_platform_ip_route_get_prune_list(self,
                                                            AF_INET6,
                                                            ifindex,
-                                                           NM_IP_ROUTE_TABLE_SYNC_MODE_ALL_PRUNE);
+                                                           NM_IP_ROUTE_TABLE_SYNC_MODE_ALL_PRUNE,
+                                                           NULL);
         success &= nm_platform_ip_route_sync(self, AF_INET6, ifindex, NULL, routes_prune, NULL);
     }
     return success;
@@ -6533,9 +6566,9 @@ nm_platform_lnk_infiniband_to_string(const NMPlatformLnkInfiniband *lnk, char *b
 const char *
 nm_platform_lnk_ip6tnl_to_string(const NMPlatformLnkIp6Tnl *lnk, char *buf, gsize len)
 {
-    char  str_local[30];
+    char  str_local[30 + NM_INET_ADDRSTRLEN];
     char  str_local1[NM_INET_ADDRSTRLEN];
-    char  str_remote[30];
+    char  str_remote[30 + NM_INET_ADDRSTRLEN];
     char  str_remote1[NM_INET_ADDRSTRLEN];
     char  str_ttl[30];
     char  str_tclass[30];
@@ -8767,6 +8800,45 @@ nm_platform_lnk_wireguard_cmp(const NMPlatformLnkWireGuard *a, const NMPlatformL
     return 0;
 }
 
+static int
+_route_objs_cmp_values(gconstpointer a, gconstpointer b, gpointer user_data)
+{
+    const NMPObject         *a_obj    = a;
+    const NMPObject         *b_obj    = b;
+    NMPlatformIPRouteCmpType cmp_type = GPOINTER_TO_INT(user_data);
+
+    nm_assert(a_obj && b_obj);
+    nm_assert(NMP_OBJECT_CAST_IP_ROUTE(a_obj) && NMP_OBJECT_CAST_IP_ROUTE(b_obj));
+
+    if (NMP_OBJECT_GET_ADDR_FAMILY(a_obj) != NMP_OBJECT_GET_ADDR_FAMILY(b_obj)) {
+        return NMP_OBJECT_GET_ADDR_FAMILY(a_obj) == AF_INET ? 1 : -1;
+    } else if (NMP_OBJECT_GET_ADDR_FAMILY(a_obj) == AF_INET) {
+        return nm_platform_ip4_route_cmp(NMP_OBJECT_CAST_IP4_ROUTE(a_obj),
+                                         NMP_OBJECT_CAST_IP4_ROUTE(b_obj),
+                                         cmp_type);
+    } else {
+        return nm_platform_ip6_route_cmp(NMP_OBJECT_CAST_IP6_ROUTE(a_obj),
+                                         NMP_OBJECT_CAST_IP6_ROUTE(b_obj),
+                                         cmp_type);
+    }
+}
+
+static int
+_route_objs_cmp(gconstpointer a, gconstpointer b, gpointer user_data)
+{
+    nm_assert(a && b);
+
+    return _route_objs_cmp_values(*((const NMPObject **) a), *((const NMPObject **) b), user_data);
+}
+
+void
+nm_platform_route_objs_sort(GPtrArray *routes_objs, NMPlatformIPRouteCmpType cmp_type)
+{
+    nm_assert(routes_objs);
+
+    g_ptr_array_sort_with_data(routes_objs, _route_objs_cmp, GINT_TO_POINTER((int) cmp_type));
+}
+
 void
 nm_platform_ip4_rt_nexthop_hash_update(const NMPlatformIP4RtNextHop *obj,
                                        gboolean                      for_id,
diff --git a/src/libnm-platform/nm-platform.h b/src/libnm-platform/nm-platform.h
index e33be813..22bf0fdb 100644
--- a/src/libnm-platform/nm-platform.h
+++ b/src/libnm-platform/nm-platform.h
@@ -2389,7 +2389,8 @@ int nm_platform_ip6_route_add(NMPlatform *self, NMPNlmFlags flags, const NMPlatf
 GPtrArray *nm_platform_ip_route_get_prune_list(NMPlatform            *self,
                                                int                    addr_family,
                                                int                    ifindex,
-                                               NMIPRouteTableSyncMode route_table_sync);
+                                               NMIPRouteTableSyncMode route_table_sync,
+                                               GPtrArray             *old_routes_objs);
 
 gboolean nm_platform_ip_route_sync(NMPlatform *self,
                                    int         addr_family,
@@ -2495,6 +2496,8 @@ int nm_platform_lnk_wireguard_cmp(const NMPlatformLnkWireGuard *a, const NMPlatf
 
 GHashTable *nm_platform_ip4_address_addr_to_hash(NMPlatform *self, int ifindex);
 
+void nm_platform_route_objs_sort(GPtrArray *routes_objs, NMPlatformIPRouteCmpType cmp_type);
+
 int nm_platform_ip4_route_cmp(const NMPlatformIP4Route *a,
                               const NMPlatformIP4Route *b,
                               NMPlatformIPRouteCmpType  cmp_type);
diff --git a/src/libnm-platform/nmp-base.h b/src/libnm-platform/nmp-base.h
index c7d487e2..3784a78e 100644
--- a/src/libnm-platform/nmp-base.h
+++ b/src/libnm-platform/nmp-base.h
@@ -211,8 +211,11 @@ nmp_object_type_to_flags(NMPObjectType obj_type)
  * @NM_IP_ROUTE_TABLE_SYNC_MODE_NONE: indicate an invalid setting.
  * @NM_IP_ROUTE_TABLE_SYNC_MODE_MAIN: only the main table is synced. For all
  *   other tables, NM won't delete any extra routes.
- * @NM_IP_ROUTE_TABLE_SYNC_MODE_FULL: NM will sync all tables, except the
- *   local table (255).
+ * @NM_IP_ROUTE_TABLE_SYNC_MODE_MAIN_AND_NM_ROUTES: only the main table is synced,
+ *   plus individual routes in other tables added by NM, leaving routes that
+ *   were not added by NM untouched.
+ * @NM_IP_ROUTE_TABLE_SYNC_MODE_ALL_EXCEPT_LOCAL: NM will sync all tables, except
+ *   the local table (255).
  * @NM_IP_ROUTE_TABLE_SYNC_MODE_ALL: NM will sync all tables, including the
  *   local table (255).
  * @NM_IP_ROUTE_TABLE_SYNC_MODE_ALL_PRUNE: NM will sync all tables (including
@@ -222,7 +225,8 @@ nmp_object_type_to_flags(NMPObjectType obj_type)
 typedef enum {
     NM_IP_ROUTE_TABLE_SYNC_MODE_NONE,
     NM_IP_ROUTE_TABLE_SYNC_MODE_MAIN,
-    NM_IP_ROUTE_TABLE_SYNC_MODE_FULL,
+    NM_IP_ROUTE_TABLE_SYNC_MODE_MAIN_AND_NM_ROUTES,
+    NM_IP_ROUTE_TABLE_SYNC_MODE_ALL_EXCEPT_LOCAL,
     NM_IP_ROUTE_TABLE_SYNC_MODE_ALL,
     NM_IP_ROUTE_TABLE_SYNC_MODE_ALL_PRUNE,
 } NMIPRouteTableSyncMode;
diff --git a/src/libnmc-setting/nm-meta-setting-desc.c b/src/libnmc-setting/nm-meta-setting-desc.c
index b3a51ba0..5568c05d 100644
--- a/src/libnmc-setting/nm-meta-setting-desc.c
+++ b/src/libnmc-setting/nm-meta-setting-desc.c
@@ -5635,7 +5635,7 @@ static const NMMetaPropertyInfo *const property_infos_CONNECTION[] = {
         .property_type =                &_pt_gobject_enum,
     ),
     PROPERTY_INFO_WITH_DESC (NM_SETTING_CONNECTION_DOWN_ON_POWEROFF,
-        .property_type =                &_pt_gobject_ternary,
+        .property_type =                &_pt_gobject_enum,
     ),
     PROPERTY_INFO_WITH_DESC (NM_SETTING_CONNECTION_SECONDARIES,
         .describe_message =
diff --git a/src/nmcli/gen-metadata-nm-settings-nmcli.xml.in b/src/nmcli/gen-metadata-nm-settings-nmcli.xml.in
index 7f5bc2c7..ae3a388b 100644
--- a/src/nmcli/gen-metadata-nm-settings-nmcli.xml.in
+++ b/src/nmcli/gen-metadata-nm-settings-nmcli.xml.in
@@ -693,8 +693,8 @@
                   values="default (-1), false (0), true (1)" />
         <property name="down-on-poweroff"
                   nmcli-description="Whether the connection will be brought down before the system is powered off.  The default value is &quot;default&quot; (-1). When the default value is specified, then the global value from NetworkManager configuration is looked up, if not set, it is considered as &quot;no&quot; (0)."
-                  format="ternary"
-                  values="true/yes/on, false/no/off, default/unknown" />
+                  format="choice (NMSettingConnectionDownOnPoweroff)"
+                  values="default (-1), no (0), yes (1)" />
         <property name="secondaries"
                   nmcli-description="List of connection UUIDs that should be activated when the base connection itself is activated. Currently, only VPN connections are supported."
                   format="list of strings" />