summary refs log tree commit diff
path: root/src/core/nm-l3cfg.c
diff options
context:
space:
mode:
authorMichael Biebl <biebl@debian.org>2026-07-03 19:53:18 +0200
committerMichael Biebl <biebl@debian.org>2026-07-03 19:53:18 +0200
commit537bfce2bda471c92caabd388589230200891509 (patch)
treeaedeccfaf0ba52c238ecf51fc009c0db5d4b60f0 /src/core/nm-l3cfg.c
parent869e9027026cdbb15d4e4a6327ff41d2697858eb (diff)
New upstream version 1.58~rc1 upstream/1.58_rc1
Diffstat (limited to 'src/core/nm-l3cfg.c')
-rw-r--r--src/core/nm-l3cfg.c593
1 files changed, 593 insertions, 0 deletions
diff --git a/src/core/nm-l3cfg.c b/src/core/nm-l3cfg.c
index 88a9c241..98a2a2a6 100644
--- a/src/core/nm-l3cfg.c
+++ b/src/core/nm-l3cfg.c
@@ -7,10 +7,15 @@
 #include "libnm-std-aux/nm-linux-compat.h"
 
 #include <net/if.h>
+#include <net/if_arp.h>
 #include "nm-compat-headers/linux/if_addr.h"
 #include <linux/if_ether.h>
 #include <linux/rtnetlink.h>
 #include <linux/fib_rules.h>
+#if HAVE_CLAT
+#include <bpf/libbpf.h>
+#include <bpf/bpf.h>
+#endif /* HAVE_CLAT */
 
 #include "libnm-core-aux-intern/nm-libnm-core-utils.h"
 #include "libnm-glib-aux/nm-prioq.h"
@@ -22,6 +27,13 @@
 #include "n-acd/src/n-acd.h"
 #include "nm-l3-ipv4ll.h"
 #include "nm-ip-config.h"
+#include "nm-core-utils.h"
+#if HAVE_CLAT
+#include "bpf/clat.h"
+NM_PRAGMA_WARNING_DISABLE("-Wcast-align")
+#include "bpf/clat.skel.h"
+NM_PRAGMA_WARNING_REENABLE
+#endif /* HAVE_CLAT */
 
 /*****************************************************************************/
 
@@ -289,6 +301,24 @@ typedef struct _NML3CfgPrivate {
         NMIPConfig *ipconfig_x[2];
     };
 
+#if HAVE_CLAT
+    /* The reserved IPv4 address for CLAT in the 192.0.0.0/28 range */
+    NMNetnsIPReservation *clat_address_4;
+    /* The IPv6 address for sending and receiving translated packets */
+    NMPlatformIP6Address clat_address_6;
+
+    /* The same addresses as above, but already committed previously */
+    NMNetnsIPReservation *clat_address_4_committed;
+    NMPlatformIP6Address  clat_address_6_committed;
+
+    /* If NULL, the BPF program hasn't been loaded or attached */
+    struct clat_bpf *clat_bpf;
+    struct bpf_link *clat_ingress_link;
+    struct bpf_link *clat_egress_link;
+
+    int clat_socket;
+#endif /* HAVE_CLAT */
+
     /* Whether we earlier configured MPTCP endpoints for the interface. */
     union {
         struct {
@@ -353,6 +383,9 @@ typedef struct _NML3CfgPrivate {
 
     bool rp_filter_handled : 1;
     bool rp_filter_set : 1;
+
+    bool clat_address_6_valid : 1;
+    bool clat_address_6_committed_valid : 1;
 } NML3CfgPrivate;
 
 struct _NML3CfgClass {
@@ -4107,6 +4140,291 @@ update_routes:
     }
 }
 
+#if HAVE_CLAT
+/**
+ * _clat_prefix_is_better:
+ * @best: current best candidate (or %NULL)
+ * @candidate: the new candidate prefix
+ * @nat64_pref: the NAT64 prefix
+ *
+ * Compare two SLAAC candidate prefixes to be used for CLAT,
+ * as recommended by draft-ietf-v6ops-claton Section 7. Apply
+ * rules 6 and 8 of the source address selection algorithm from
+ * RFC 6724, Section 5.
+ *
+ * Returns %TRUE if @candidate is better than @best.
+ */
+static gboolean
+_clat_prefix_is_better(const NMPlatformIP6Address *best,
+                       const NMPlatformIP6Address *candidate,
+                       const struct in6_addr      *nat64_pref)
+{
+    guint    nat64_pref_label;
+    gboolean best_label_match;
+    gboolean cand_label_match;
+    guint    best_prefix_len;
+    guint    cand_prefix_len;
+
+    if (!best)
+        return TRUE;
+
+    /* Rule 6: prefer the address whose RFC 6724 label matches
+     * the label of the NAT64 prefix. */
+    nat64_pref_label = nm_ip6_addr_rfc6724_label(nat64_pref);
+    best_label_match = nm_ip6_addr_rfc6724_label(&best->address) == nat64_pref_label;
+    cand_label_match = nm_ip6_addr_rfc6724_label(&candidate->address) == nat64_pref_label;
+
+    if (cand_label_match && !best_label_match)
+        return TRUE;
+    else if (best_label_match && !cand_label_match)
+        return FALSE;
+
+    /* Rule 8: longest matching prefix with the NAT64 prefix. */
+    best_prefix_len = nm_ip6_addr_common_prefix_len(&best->address, nat64_pref);
+    cand_prefix_len = nm_ip6_addr_common_prefix_len(&candidate->address, nat64_pref);
+    if (cand_prefix_len != best_prefix_len)
+        return cand_prefix_len > best_prefix_len;
+
+    return FALSE;
+}
+#endif /* HAVE_CLAT */
+
+static void
+_l3cfg_update_clat_config(NML3Cfg             *self,
+                          NML3ConfigData      *l3cd,
+                          const L3ConfigData **l3_config_datas_arr,
+                          guint                l3_config_datas_len)
+{
+#if !HAVE_CLAT
+    return;
+#else
+    struct in6_addr           pref64;
+    guint32                   pref64_plen;
+    gboolean                  clat_enabled = FALSE;
+    const NMPlatformIP4Route *ip4_route;
+    NMDedupMultiIter          iter;
+
+    switch (nm_l3_config_data_get_clat_config(l3cd)) {
+    case NM_SETTING_IP4_CONFIG_CLAT_FORCE:
+        clat_enabled = TRUE;
+        break;
+    case NM_SETTING_IP4_CONFIG_CLAT_NO:
+        clat_enabled = FALSE;
+        break;
+    case NM_SETTING_IP4_CONFIG_CLAT_AUTO:
+        clat_enabled = TRUE;
+        /* disable if there is a native IPv4 gateway */
+        nm_l3_config_data_iter_ip4_route_for_each (&iter, l3cd, &ip4_route) {
+            if (ip4_route->network == INADDR_ANY && ip4_route->plen == 0
+                && ip4_route->gateway != INADDR_ANY) {
+                clat_enabled = FALSE;
+                break;
+            }
+        }
+        break;
+    case NM_SETTING_IP4_CONFIG_CLAT_DEFAULT:
+        nm_assert_not_reached();
+        clat_enabled = TRUE;
+        break;
+    }
+
+    if (clat_enabled && nm_l3_config_data_get_pref64_valid(l3cd)) {
+        NMPlatformIPXRoute          rx;
+        NMIPAddrTyped               best_v6_gateway;
+        const NMPlatformIP6Route   *best_v6_route;
+        const NMPlatformIP6Address *ip6_entry;
+        struct in6_addr             ip6;
+        const char                 *network_id;
+        char                        buf[512];
+        guint32                     route4_metric = NM_PLATFORM_ROUTE_METRIC_DEFAULT_IP4;
+        guint                       i;
+
+        /* If we have a valid NAT64 prefix, configure in kernel:
+         *
+         * - a CLAT IPv4 address (192.0.0.x)
+         * - a IPv4 default route via the best IPv6 gateway
+         *
+         * We also set clat_address_6 as an additional /64 IPv6 address
+         * determined according to https://www.rfc-editor.org/rfc/rfc6877#section-6.3 .
+         * This address is used for sending and receiving translated packets,
+         * but is not configured in kernel to avoid that it gets used by applications.
+         * Later in _l3_commit_pref64() we use IPV6_JOIN_ANYCAST to let the kernel
+         * handle ND for the address.
+         */
+
+        nm_l3_config_data_get_pref64(l3cd, &pref64, &pref64_plen);
+        network_id = nm_l3_config_data_get_network_id(l3cd);
+
+        if (!self->priv.p->clat_address_6_valid && network_id) {
+            const NMPlatformIP6Address *best_prefix = NULL;
+
+            /* Select the best SLAAC prefix for the CLAT address per
+             * draft-ietf-v6ops-claton-14 Section 7 */
+            nm_l3_config_data_iter_ip6_address_for_each (&iter, l3cd, &ip6_entry) {
+                if (ip6_entry->addr_source == NM_IP_CONFIG_SOURCE_NDISC && ip6_entry->plen == 64) {
+                    if (_clat_prefix_is_better(best_prefix, ip6_entry, &pref64))
+                        best_prefix = ip6_entry;
+                }
+            }
+
+            if (best_prefix) {
+                ip6 = best_prefix->address;
+
+                nm_utils_ipv6_addr_set_stable_privacy(NM_UTILS_STABLE_TYPE_CLAT,
+                                                      &ip6,
+                                                      nm_l3cfg_get_ifname(self, TRUE),
+                                                      network_id,
+                                                      0);
+                self->priv.p->clat_address_6 = (NMPlatformIP6Address) {
+                    .ifindex      = self->priv.ifindex,
+                    .address      = ip6,
+                    .peer_address = ip6,
+                    .addr_source  = NM_IP_CONFIG_SOURCE_CLAT,
+                    .plen         = best_prefix->plen,
+                };
+
+                _LOGT("clat: using IPv6 address %s", nm_inet6_ntop(&ip6, buf));
+
+                self->priv.p->clat_address_6_valid = TRUE;
+            }
+        }
+
+        /* Don't get a v4 address if we have no v6 address (otherwise, we could
+         * potentially create broken v4 connectivity) */
+        if (!self->priv.p->clat_address_6_valid) {
+            _LOGW("CLAT is currently only supported when SLAAC is in use.");
+            /* Deallocate the v4 address unless it's the committed one */
+            if (self->priv.p->clat_address_4 != self->priv.p->clat_address_4_committed) {
+                nm_clear_pointer(&self->priv.p->clat_address_4, nm_netns_ip_reservation_release);
+            } else {
+                self->priv.p->clat_address_4 = NULL;
+            }
+        } else if (!self->priv.p->clat_address_4) {
+            /* We need a v4 /32 */
+            self->priv.p->clat_address_4 =
+                nm_netns_ip_reservation_get(self->priv.netns, NM_NETNS_IP_RESERVATION_TYPE_CLAT);
+        }
+
+        {
+            const NMPlatformIP4Route *r4;
+            guint32                   metric  = 0;
+            guint32                   penalty = 0;
+
+            /* Find the IPv4 metric for the CLAT default route.
+             * If there is another non-CLAT default route on the device, use the
+             * same metric + 1, so that native connectivity is always preferred.
+             * Otherwise, use the metric from the connection profile.
+             */
+
+            r4 = NMP_OBJECT_CAST_IP4_ROUTE(nm_l3_config_data_get_best_default_route(l3cd, AF_INET));
+
+            if (r4) {
+                route4_metric = nm_add_clamped_u32(r4->metric, 1u);
+            } else {
+                for (i = 0; i < l3_config_datas_len; i++) {
+                    const L3ConfigData *l3cd_data = l3_config_datas_arr[i];
+
+                    if (l3cd_data->default_route_metric_4 != NM_PLATFORM_ROUTE_METRIC_DEFAULT_IP4) {
+                        metric = l3cd_data->default_route_metric_4;
+                    }
+                    if (l3cd_data->default_route_penalty_4 != 0) {
+                        penalty = l3cd_data->default_route_penalty_4;
+                    }
+                }
+                route4_metric = nm_add_clamped_u32(metric, penalty);
+            }
+        }
+
+        if (self->priv.p->clat_address_4) {
+            best_v6_route = NMP_OBJECT_CAST_IP6_ROUTE(
+                nm_l3_config_data_get_direct_route_for_host(l3cd, AF_INET6, &pref64));
+            if (!best_v6_route) {
+                best_v6_route = NMP_OBJECT_CAST_IP6_ROUTE(
+                    nm_l3_config_data_get_best_default_route(l3cd, AF_INET6));
+            }
+            if (best_v6_route) {
+                NMPlatformIP4Address addr = {
+                    .ifindex      = self->priv.ifindex,
+                    .address      = self->priv.p->clat_address_4->addr,
+                    .peer_address = self->priv.p->clat_address_4->addr,
+                    .addr_source  = NM_IP_CONFIG_SOURCE_CLAT,
+                    .plen         = 32,
+                };
+                const NMPlatformLink *pllink;
+                guint                 mtu = 0;
+                guint                 val = 0;
+
+                best_v6_gateway.addr_family = AF_INET6;
+                best_v6_gateway.addr.addr6  = best_v6_route->gateway;
+
+                /* Determine the IPv6 MTU of the interface. Unfortunately,
+                 * the logic to set the MTU is in NMDevice and here we need
+                 * some duplication to find the actual value.
+                 * TODO: move the MTU handling into l3cfg. */
+
+                /* Get the link MTU */
+                pllink = nm_l3cfg_get_pllink(self, TRUE);
+                if (pllink)
+                    mtu = pllink->mtu;
+                if (mtu == 0)
+                    mtu = 1500;
+
+                /* Update it with the IPv6 MTU value from the connection
+                 * or from RA */
+                val = nm_l3_config_data_get_ip6_mtu_static(l3cd);
+                if (val == 0) {
+                    val = nm_l3_config_data_get_ip6_mtu_ra(l3cd);
+                }
+                if (val != 0 && val < mtu) {
+                    mtu = val;
+                }
+                if (mtu < 1280)
+                    mtu = 1280;
+
+                /* Leave 20 additional bytes for the ipv4 -> ipv6 header translation,
+                 * plus 8 for a potential fragmentation extension header */
+                mtu -= 28;
+
+                rx.r4 = (NMPlatformIP4Route) {
+                    .ifindex       = self->priv.ifindex,
+                    .rt_source     = NM_IP_CONFIG_SOURCE_CLAT,
+                    .network       = 0, /* default route */
+                    .plen          = 0,
+                    .table_coerced = nm_platform_route_table_coerce(RT_TABLE_MAIN),
+                    .scope_inv     = nm_platform_route_scope_inv(RT_SCOPE_UNIVERSE),
+                    .type_coerced  = nm_platform_route_type_coerce(RTN_UNICAST),
+                    .pref_src      = self->priv.p->clat_address_4->addr,
+                    .via           = best_v6_gateway,
+                    .metric        = route4_metric,
+                    .mtu           = mtu,
+                };
+                nm_platform_ip_route_normalize(AF_INET, &rx.rx);
+                if (!nm_l3_config_data_lookup_route(l3cd, AF_INET, &rx.rx)) {
+                    nm_l3_config_data_add_route_4(l3cd, &rx.r4);
+                }
+
+                _LOGT("clat: route %s", nm_platform_ip4_route_to_string(&rx.r4, buf, sizeof(buf)));
+
+                nm_l3_config_data_add_address_4(l3cd, &addr);
+            } else {
+                _LOGW("Couldn't find a good ipv6 route! Unable to set up CLAT!");
+            }
+        }
+
+        if (self->priv.p->clat_address_4 && self->priv.p->clat_address_6_valid) {
+            nm_l3_config_data_set_clat_state(l3cd,
+                                             TRUE,
+                                             &self->priv.p->clat_address_6.address,
+                                             &pref64,
+                                             pref64_plen,
+                                             self->priv.p->clat_address_4->addr);
+        } else {
+            nm_l3_config_data_set_clat_state(l3cd, FALSE, NULL, NULL, 0, INADDR_ANY);
+        }
+    }
+#endif /* HAVE_CLAT */
+}
+
 static void
 _l3cfg_update_combined_config(NML3Cfg               *self,
                               gboolean               to_commit,
@@ -4220,6 +4538,8 @@ _l3cfg_update_combined_config(NML3Cfg               *self,
                                     &hook_data);
         }
 
+        _l3cfg_update_clat_config(self, l3cd, l3_config_datas_arr, l3_config_datas_len);
+
         if (self->priv.ifindex == NM_LOOPBACK_IFINDEX) {
             NMPlatformIPXAddress ax;
             NMPlatformIPXRoute   rx;
@@ -4280,6 +4600,18 @@ _l3cfg_update_combined_config(NML3Cfg               *self,
     if (nm_l3_config_data_equal(l3cd, self->priv.p->combined_l3cd_merged))
         goto out;
 
+#if HAVE_CLAT
+    if (!l3cd) {
+        self->priv.p->clat_address_6_valid = FALSE;
+        /* Deallocate the v4 address unless it's the commited one */
+        if (self->priv.p->clat_address_4 != self->priv.p->clat_address_4_committed) {
+            nm_clear_pointer(&self->priv.p->clat_address_4, nm_netns_ip_reservation_release);
+        } else {
+            self->priv.p->clat_address_4 = NULL;
+        }
+    }
+#endif /* HAVE_CLAT */
+
     l3cd_old                           = g_steal_pointer(&self->priv.p->combined_l3cd_merged);
     self->priv.p->combined_l3cd_merged = nm_l3_config_data_seal(g_steal_pointer(&l3cd));
     merged_changed                     = TRUE;
@@ -5379,6 +5711,251 @@ _l3_commit_one(NML3Cfg              *self,
     _failedobj_handle_routes(self, addr_family, routes_failed);
 }
 
+#if HAVE_CLAT
+static void
+_l3_clat_destroy(NML3Cfg *self)
+{
+    char buf[100];
+    int  err;
+
+    if (self->priv.p->clat_bpf) {
+        const struct clat_stats *s = &self->priv.p->clat_bpf->bss->stats;
+
+        _LOGT("clat: stats:"
+              " egress (v4 to v6): tcp %" G_GUINT64_FORMAT ", udp %" G_GUINT64_FORMAT
+              ", icmp %" G_GUINT64_FORMAT ", other %" G_GUINT64_FORMAT
+              ", dropped %" G_GUINT64_FORMAT "; ingress (v6 to v4): tcp %" G_GUINT64_FORMAT
+              ", udp %" G_GUINT64_FORMAT ", icmp %" G_GUINT64_FORMAT ", other %" G_GUINT64_FORMAT
+              ", fragment %" G_GUINT64_FORMAT ", dropped %" G_GUINT64_FORMAT,
+              (guint64) s->egress_tcp,
+              (guint64) s->egress_udp,
+              (guint64) s->egress_icmp,
+              (guint64) s->egress_other,
+              (guint64) s->egress_dropped,
+              (guint64) s->ingress_tcp,
+              (guint64) s->ingress_udp,
+              (guint64) s->ingress_icmp,
+              (guint64) s->ingress_other,
+              (guint64) s->ingress_fragment,
+              (guint64) s->ingress_dropped);
+    }
+
+    if (self->priv.p->clat_ingress_link) {
+        err = bpf_link__destroy(self->priv.p->clat_ingress_link);
+        if (err != 0) {
+            libbpf_strerror(err, buf, sizeof(buf));
+            _LOGD("clat: failed to destroy the ingress link");
+        }
+        self->priv.p->clat_ingress_link = NULL;
+    }
+
+    if (self->priv.p->clat_egress_link) {
+        err = bpf_link__destroy(self->priv.p->clat_egress_link);
+        if (err != 0) {
+            libbpf_strerror(err, buf, sizeof(buf));
+            _LOGD("clat: failed to destroy the egress link");
+        }
+        self->priv.p->clat_egress_link = NULL;
+    }
+
+    nm_clear_pointer(&self->priv.p->clat_bpf, clat_bpf__destroy);
+}
+
+static void
+_l3_commit_pref64(NML3Cfg *self, NML3CfgCommitType commit_type)
+{
+    int                    err  = 0;
+    const NML3ConfigData  *l3cd = self->priv.p->combined_l3cd_commited;
+    struct in6_addr        _l3cd_pref64_inner;
+    const struct in6_addr *l3cd_pref64 = NULL;
+    guint32                l3cd_pref64_plen;
+    char                   buf[100];
+    struct clat_config     clat_config;
+    gboolean               v6_changed;
+    const NMPlatformLink  *pllink;
+    gboolean               has_ethernet_header = FALSE;
+
+    if (l3cd && nm_l3_config_data_get_pref64(l3cd, &_l3cd_pref64_inner, &l3cd_pref64_plen)) {
+        l3cd_pref64 = &_l3cd_pref64_inner;
+    }
+
+    if (l3cd_pref64 && self->priv.p->clat_address_4 && self->priv.p->clat_address_6_valid) {
+        pllink = nm_l3cfg_get_pllink(self, TRUE);
+        if (!pllink) {
+            has_ethernet_header = TRUE;
+        } else {
+            switch (pllink->arptype) {
+            case ARPHRD_ETHER:
+                has_ethernet_header = TRUE;
+                break;
+            case ARPHRD_NONE:
+            case ARPHRD_PPP:
+            case ARPHRD_RAWIP:
+                has_ethernet_header = FALSE;
+                break;
+            default:
+                _LOGD("clat: unknown ARP type %u, assuming the interface uses no L2 header",
+                      pllink->arptype);
+                has_ethernet_header = FALSE;
+            }
+        }
+
+        if (!self->priv.p->clat_bpf) {
+            _LOGT("clat: attaching the BPF program");
+
+            self->priv.p->clat_bpf = clat_bpf__open();
+            if (!self->priv.p->clat_bpf) {
+                libbpf_strerror(errno, buf, sizeof(buf));
+                _LOGW("clat: failed to open the BPF program: %s", buf);
+                return;
+            }
+
+            /* Only load the programs for the right L2 type */
+            bpf_program__set_autoload(self->priv.p->clat_bpf->progs.nm_clat_ingress_eth,
+                                      has_ethernet_header);
+            bpf_program__set_autoload(self->priv.p->clat_bpf->progs.nm_clat_egress_eth,
+                                      has_ethernet_header);
+            bpf_program__set_autoload(self->priv.p->clat_bpf->progs.nm_clat_ingress_rawip,
+                                      !has_ethernet_header);
+            bpf_program__set_autoload(self->priv.p->clat_bpf->progs.nm_clat_egress_rawip,
+                                      !has_ethernet_header);
+
+            if (clat_bpf__load(self->priv.p->clat_bpf)) {
+                libbpf_strerror(errno, buf, sizeof(buf));
+                _LOGW("clat: failed to load the BPF program: %s", buf);
+                nm_clear_pointer(&self->priv.p->clat_bpf, clat_bpf__destroy);
+                return;
+            }
+
+            self->priv.p->clat_ingress_link = bpf_program__attach_tcx(
+                has_ethernet_header ? self->priv.p->clat_bpf->progs.nm_clat_ingress_eth
+                                    : self->priv.p->clat_bpf->progs.nm_clat_ingress_rawip,
+                self->priv.ifindex,
+                NULL);
+            if (!self->priv.p->clat_ingress_link) {
+                libbpf_strerror(errno, buf, sizeof(buf));
+                _LOGW("clat: failed to attach the ingress program: %s", buf);
+                return;
+            }
+
+            self->priv.p->clat_egress_link = bpf_program__attach_tcx(
+                has_ethernet_header ? self->priv.p->clat_bpf->progs.nm_clat_egress_eth
+                                    : self->priv.p->clat_bpf->progs.nm_clat_egress_rawip,
+                self->priv.ifindex,
+                NULL);
+            if (!self->priv.p->clat_egress_link) {
+                libbpf_strerror(errno, buf, sizeof(buf));
+                _LOGW("clat: failed to attach the egress program: %s", buf);
+                return;
+            }
+
+            _LOGT("clat: program attached successfully");
+        }
+
+        /* Pass configuration to the BPF program */
+        memset(&clat_config, 0, sizeof(clat_config));
+        clat_config.local_v4.s_addr         = self->priv.p->clat_address_4->addr;
+        clat_config.local_v6                = self->priv.p->clat_address_6.address;
+        clat_config.pref64                  = *l3cd_pref64;
+        clat_config.pref64_len              = l3cd_pref64_plen;
+        self->priv.p->clat_bpf->bss->config = clat_config;
+
+        if (self->priv.p->clat_socket < 0) {
+            self->priv.p->clat_socket = socket(AF_INET6, SOCK_RAW, IPPROTO_ICMPV6);
+            if (self->priv.p->clat_socket < 0) {
+                _LOGW("clat: couldn't create the socket: %s", nm_strerror_native(errno));
+            }
+        }
+        if (self->priv.p->clat_socket >= 0) {
+            err = setsockopt(self->priv.p->clat_socket,
+                             SOL_SOCKET,
+                             SO_BINDTOIFINDEX,
+                             &self->priv.ifindex,
+                             sizeof(self->priv.ifindex));
+            if (err < 0) {
+                _LOGW("clat: couldn't bind the socket: %s", nm_strerror_native(errno));
+            }
+        }
+
+        v6_changed =
+            (self->priv.p->clat_address_6_valid != self->priv.p->clat_address_6_committed_valid)
+            || (self->priv.p->clat_address_6_valid && self->priv.p->clat_address_6_committed_valid
+                && memcmp(&self->priv.p->clat_address_6.address,
+                          &self->priv.p->clat_address_6_committed.address,
+                          sizeof(self->priv.p->clat_address_6_committed.address)));
+
+        if (self->priv.p->clat_socket > 0 && v6_changed) {
+            struct ipv6_mreq mreq = {.ipv6mr_interface = self->priv.ifindex};
+
+            if (self->priv.p->clat_address_6_committed_valid) {
+                mreq.ipv6mr_multiaddr = self->priv.p->clat_address_6_committed.address;
+
+                err = setsockopt(self->priv.p->clat_socket,
+                                 SOL_IPV6,
+                                 IPV6_LEAVE_ANYCAST,
+                                 &mreq,
+                                 sizeof(mreq));
+                if (err < 0) {
+                    _LOGW("clat: couldn't leave the anycast group: %s", nm_strerror_native(errno));
+                }
+            }
+
+            if (self->priv.p->clat_address_6_valid) {
+                /* As per draft-ietf-v6ops-claton-14, hosts must perform duplicate
+                 * addresses detection (DAD) on the generated CLAT IPv6 address. This is
+                 * necessary not only to avoid address collisions but also because some
+                 * networks drop traffic from addresses that have not done DAD.
+                 * Since doing true DAD adds complexity, adopt the same approach as
+                 * Android: start DAD by sending a neighbor solicitation and don't wait
+                 * for any reply. This avoids the problem with dropped traffic; it
+                 * doesn't help with collisions, but collisions are anyway very unlikely
+                 * because the interface identifier is a random 64-bit value.
+                 */
+                nm_utils_ipv6_dad_send(&self->priv.p->clat_address_6.address,
+                                       self->priv.ifindex,
+                                       pllink ? pllink->arptype : ARPHRD_ETHER);
+
+                mreq.ipv6mr_multiaddr = self->priv.p->clat_address_6.address;
+
+                err = setsockopt(self->priv.p->clat_socket,
+                                 SOL_IPV6,
+                                 IPV6_JOIN_ANYCAST,
+                                 &mreq,
+                                 sizeof(mreq));
+                if (err < 0) {
+                    _LOGW("clat: couldn't join the anycast group: %s", nm_strerror_native(errno));
+                }
+            }
+        }
+    } else {
+        if (self->priv.p->clat_bpf) {
+            _l3_clat_destroy(self);
+        }
+
+        /* Committed will get cleaned up below */
+        self->priv.p->clat_address_6_valid = FALSE;
+
+        nm_clear_fd(&self->priv.p->clat_socket);
+
+        /* Deallocate the v4 address. Committed address will get cleaned up below,
+           but we need to make sure there's no double-free */
+        if (self->priv.p->clat_address_4 != self->priv.p->clat_address_4_committed) {
+            nm_clear_pointer(&self->priv.p->clat_address_4, nm_netns_ip_reservation_release);
+        } else {
+            self->priv.p->clat_address_4 = NULL;
+        }
+    }
+
+    /* Record the new state */
+    if (self->priv.p->clat_address_4_committed != self->priv.p->clat_address_4) {
+        nm_clear_pointer(&self->priv.p->clat_address_4_committed, nm_netns_ip_reservation_release);
+        self->priv.p->clat_address_4_committed = self->priv.p->clat_address_4;
+    }
+    self->priv.p->clat_address_6_committed       = self->priv.p->clat_address_6;
+    self->priv.p->clat_address_6_committed_valid = self->priv.p->clat_address_6_valid;
+}
+#endif /* HAVE_CLAT */
+
 static void
 _l3_commit(NML3Cfg *self, NML3CfgCommitType commit_type, gboolean is_idle)
 {
@@ -5461,6 +6038,10 @@ _l3_commit(NML3Cfg *self, NML3CfgCommitType commit_type, gboolean is_idle)
 
     _l3_acd_data_process_changes(self);
 
+#if HAVE_CLAT
+    _l3_commit_pref64(self, commit_type);
+#endif /* HAVE_CLAT */
+
     nm_assert(self->priv.p->commit_reentrant_count == 1);
     self->priv.p->commit_reentrant_count--;
 
@@ -5842,6 +6423,10 @@ nm_l3cfg_init(NML3Cfg *self)
 {
     self->priv.p = G_TYPE_INSTANCE_GET_PRIVATE(self, NM_TYPE_L3CFG, NML3CfgPrivate);
 
+#if HAVE_CLAT
+    self->priv.p->clat_socket = -1;
+#endif /* HAVE_CLAT */
+
     c_list_init(&self->priv.p->acd_lst_head);
     c_list_init(&self->priv.p->acd_event_notify_lst_head);
     c_list_init(&self->priv.p->commit_type_lst_head);
@@ -5950,6 +6535,14 @@ finalize(GObject *object)
     if (changed)
         nmp_global_tracker_sync_mptcp_addrs(self->priv.global_tracker, FALSE);
 
+#if HAVE_CLAT
+    self->priv.p->clat_address_4_committed = NULL;
+    nm_clear_pointer(&self->priv.p->clat_address_4, nm_netns_ip_reservation_release);
+    nm_clear_fd(&self->priv.p->clat_socket);
+    if (self->priv.p->clat_bpf) {
+        _l3_clat_destroy(self);
+    }
+#endif
     g_clear_object(&self->priv.netns);
     g_clear_object(&self->priv.platform);
     nm_clear_pointer(&self->priv.global_tracker, nmp_global_tracker_unref);