summary refs log tree commit diff
path: root/src/core
diff options
context:
space:
mode:
authorMichael Biebl <biebl@debian.org>2024-12-25 20:36:29 +0100
committerMichael Biebl <biebl@debian.org>2024-12-25 20:36:29 +0100
commite465722b908aa870bdc293b9a417d3c15294aa6d (patch)
tree7c959d3b73f427d6c40522a7464eaa4531ef2f05 /src/core
parent56928734cbcf1d3a02fae4f152a541df1b04e04a (diff)
New upstream version 1.50.1 upstream/1.50.1
Diffstat (limited to 'src/core')
-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
15 files changed, 501 insertions, 89 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",