summary refs log tree commit diff
path: root/src/core
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
parent869e9027026cdbb15d4e4a6327ff41d2697858eb (diff)
New upstream version 1.58~rc1 upstream/1.58_rc1
Diffstat (limited to 'src/core')
-rw-r--r--src/core/bpf/clat.bpf.c1203
-rw-r--r--src/core/bpf/clat.h30
-rw-r--r--src/core/bpf/meson.build239
-rw-r--r--src/core/devices/nm-device-bond.c13
-rw-r--r--src/core/devices/nm-device-ethernet.c12
-rw-r--r--src/core/devices/nm-device-macvlan.c7
-rw-r--r--src/core/devices/nm-device-veth.c2
-rw-r--r--src/core/devices/nm-device-vxlan.c4
-rw-r--r--src/core/devices/nm-device.c671
-rw-r--r--src/core/devices/nm-device.h1
-rw-r--r--src/core/devices/wifi/nm-device-iwd.c33
-rw-r--r--src/core/devices/wifi/nm-device-wifi.c174
-rw-r--r--src/core/devices/wifi/nm-wifi-ap.c18
-rw-r--r--src/core/devices/wifi/nm-wifi-utils.c40
-rw-r--r--src/core/devices/wifi/nm-wifi-utils.h4
-rw-r--r--src/core/devices/wifi/tests/test-devices-wifi.c46
-rw-r--r--src/core/devices/wwan/nm-modem-manager.c11
-rw-r--r--src/core/dhcp/README.next.md10
-rw-r--r--src/core/dhcp/nm-dhcp-client.c45
-rw-r--r--src/core/dhcp/nm-dhcp-client.h5
-rw-r--r--src/core/dhcp/nm-dhcp-dhclient-utils.c763
-rw-r--r--src/core/dhcp/nm-dhcp-dhclient-utils.h38
-rw-r--r--src/core/dhcp/nm-dhcp-dhclient.c741
-rw-r--r--src/core/dhcp/nm-dhcp-helper.c3
-rw-r--r--src/core/dhcp/nm-dhcp-listener.c3
-rw-r--r--src/core/dhcp/nm-dhcp-manager.c35
-rw-r--r--src/core/dhcp/nm-dhcp-nettools.c28
-rw-r--r--src/core/dhcp/nm-dhcp-systemd.c48
-rw-r--r--src/core/dhcp/nm-dhcp-utils.c58
-rw-r--r--src/core/dhcp/nm-dhcp-utils.h6
-rw-r--r--src/core/dhcp/tests/meson.build1
-rw-r--r--src/core/dhcp/tests/test-dhclient-commented-duid.leases2
-rw-r--r--src/core/dhcp/tests/test-dhclient-duid.leases2
-rw-r--r--src/core/dhcp/tests/test-dhcp-dhclient.c1478
-rw-r--r--src/core/dns/nm-dns-manager.c2
-rw-r--r--src/core/main.c22
-rw-r--r--src/core/meson.build17
-rw-r--r--src/core/ndisc/nm-lndp-ndisc.c35
-rw-r--r--src/core/ndisc/nm-ndisc-private.h2
-rw-r--r--src/core/ndisc/nm-ndisc.c152
-rw-r--r--src/core/ndisc/nm-ndisc.h27
-rw-r--r--src/core/nm-config.c35
-rw-r--r--src/core/nm-core-utils.c470
-rw-r--r--src/core/nm-core-utils.h16
-rw-r--r--src/core/nm-ip-config.c111
-rw-r--r--src/core/nm-l3-config-data.c297
-rw-r--r--src/core/nm-l3-config-data.h44
-rw-r--r--src/core/nm-l3cfg.c593
-rw-r--r--src/core/nm-manager.c85
-rw-r--r--src/core/nm-netns.c44
-rw-r--r--src/core/nm-netns.h1
-rw-r--r--src/core/nm-pacrunner-manager.c54
-rw-r--r--src/core/nm-policy.c14
-rw-r--r--src/core/ppp/nm-pppd-plugin.c2
-rw-r--r--src/core/settings/nm-settings-connection.c2
-rw-r--r--src/core/settings/nm-settings.c2
-rw-r--r--src/core/settings/plugins/ifcfg-rh/nms-ifcfg-rh-reader.c51
-rw-r--r--src/core/settings/plugins/ifcfg-rh/nms-ifcfg-rh-writer.c19
-rw-r--r--src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-Test_Write_WiFi_AP_Mode.cexpected3
-rw-r--r--src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-Test_Write_WiFi_Band_6ghz.cexpected18
-rw-r--r--src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-Test_Write_WiFi_Band_a.cexpected (renamed from src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-Test_Write_WiFi_Band_A.cexpected)2
-rw-r--r--src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-test-wifi-band-6ghz13
-rw-r--r--src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-test-wifi-band-6ghz-channel-mismatch9
-rw-r--r--src/core/settings/plugins/ifcfg-rh/tests/test-ifcfg-rh.c93
-rw-r--r--src/core/settings/plugins/keyfile/nms-keyfile-writer.c4
-rw-r--r--src/core/supplicant/nm-supplicant-config.c98
-rw-r--r--src/core/supplicant/nm-supplicant-interface.c15
-rw-r--r--src/core/supplicant/nm-supplicant-interface.h1
-rw-r--r--src/core/supplicant/nm-supplicant-settings-verify.c3
-rw-r--r--src/core/tests/config/NetworkManager-warn.conf2
-rw-r--r--src/core/tests/config/NetworkManager.conf2
-rw-r--r--src/core/tests/config/test-config.c4
-rw-r--r--src/core/tests/test-core.c105
-rw-r--r--src/core/tests/test-netns.c39
-rw-r--r--src/core/tests/test-systemd.c37
-rw-r--r--src/core/vpn/nm-vpn-connection.c8
-rw-r--r--src/core/vpn/nm-vpn-manager.c35
77 files changed, 4482 insertions, 3880 deletions
diff --git a/src/core/bpf/clat.bpf.c b/src/core/bpf/clat.bpf.c
new file mode 100644
index 00000000..97d74a61
--- /dev/null
+++ b/src/core/bpf/clat.bpf.c
@@ -0,0 +1,1203 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/* Copyright 2021 Toke Høiland-Jørgensen <toke@toke.dk> */
+/* Copyright 2025 Mary Strodl <mstrodl@csh.rit.edu> */
+/* Copyright 2026 Beniamino Galvani <bgalvani@redhat.com> */
+
+/**
+ * This is an implementation of a CLAT in eBPF. BPF is a different environment
+ * than the rest of NetworkManager, and we don't have access to most of the
+ * C standard library, so some things might look a little different from what
+ * you're used to.
+ *
+ * Check out src/core/bpf/meson.build to see how this gets built.
+ **/
+
+#include <linux/bpf.h>
+#include <linux/icmp.h>
+#include <linux/icmpv6.h>
+#include <linux/ip.h>
+#include <linux/ipv6.h>
+#include <linux/in.h>
+#include <linux/in6.h>
+#include <linux/pkt_cls.h>
+#include <linux/tcp.h>
+#include <linux/udp.h>
+#include <linux/if_ether.h>
+#include <stdbool.h>
+
+#include <bpf/bpf_endian.h>
+#include <bpf/bpf_helpers.h>
+
+#include "clat.h"
+
+char _license[] SEC("license") = "GPL";
+
+struct clat_config config;
+struct clat_stats  stats;
+
+#ifdef DEBUG
+/* Note: when enabling debugging, you also need to add CAP_PERFMON
+ * to the CapabilityBoundingSet of the NM systemd unit. The messages
+ * will be printed to /sys/kernel/debug/tracing/trace_pipe */
+#define DBG(fmt, ...)                                              \
+    ({                                                             \
+        char ____fmt[] = "clat: " fmt;                             \
+        bpf_trace_printk(____fmt, sizeof(____fmt), ##__VA_ARGS__); \
+    })
+#else
+#define DBG(fmt, ...)
+#endif
+
+/* Macros to read the sk_buff data* pointers, preventing the compiler
+ * from generating a 32-bit register spill. */
+#define SKB_ACCESS_MEMBER_32(_skb, member)                                  \
+    ({                                                                      \
+        void *ptr;                                                          \
+                                                                            \
+        asm volatile("%0 = *(u32 *)(%1 + %2)"                               \
+                     : "=r"(ptr)                                            \
+                     : "r"(_skb), "i"(offsetof(struct __sk_buff, member))); \
+                                                                            \
+        ptr;                                                                \
+    })
+
+#define SKB_DATA(_skb)     SKB_ACCESS_MEMBER_32(_skb, data)
+#define SKB_DATA_END(_skb) SKB_ACCESS_MEMBER_32(_skb, data_end)
+
+struct icmpv6_pseudo {
+    struct in6_addr saddr;
+    struct in6_addr daddr;
+    __u32           len;
+    __u8            padding[3];
+    __u8            nh;
+} __attribute__((packed));
+
+struct ip6_frag {
+    __u8  nexthdr;
+    __u8  reserved;
+    __u16 offset;
+    __u32 identification;
+} __attribute__((packed));
+
+#define L2_H_LEN(has_eth) (has_eth ? sizeof(struct ethhdr) : 0)
+#define IP_H_LEN          (sizeof(struct iphdr))
+#define IP6_H_LEN         (sizeof(struct ipv6hdr))
+#define IP6_FRAG_H_LEN    (sizeof(struct ip6_frag))
+#define ICMP_H_LEN        (sizeof(struct icmphdr))
+#define ICMP6_H_LEN       (sizeof(struct icmp6hdr))
+
+#define ensure_header(header, skb, data, data_end, offset) \
+    _ensure_header((void **) header, (skb), (data), (data_end), sizeof(**(header)), (offset))
+
+/*
+ * Verifies that the header at offset @offset and with size @size can
+ * be accessed, and assigns the pointer to @header. In case the data
+ * is not available, the function tries to pull it. Note that all packet
+ * pointers must be refreshed after calling this function.
+ */
+static __always_inline bool
+_ensure_header(void            **header,
+               struct __sk_buff *skb,
+               void            **data,
+               void            **data_end,
+               unsigned          size,
+               unsigned          offset)
+{
+    if (*data + offset + size > *data_end) {
+        bpf_skb_pull_data(skb, offset + size);
+        *data     = SKB_DATA(skb);
+        *data_end = SKB_DATA_END(skb);
+    }
+
+    if (*data + offset + size > *data_end)
+        return false;
+
+    *header = *data + offset;
+    return true;
+}
+
+/* This function must be declared as inline because the BPF calling
+ * convention only supports up to 5 function arguments. */
+static __always_inline void
+update_l4_checksum(struct __sk_buff *skb,
+                   struct ipv6hdr   *ip6h,
+                   struct iphdr     *iph,
+                   bool              has_eth,
+                   bool              v4to6,
+                   bool              is_inner,
+                   bool              is_v6_fragment,
+                   __u32            *csum_diff)
+{
+    int   flags = BPF_F_PSEUDO_HDR;
+    __u16 offset;
+    __u32 csum;
+    int   ip_type;
+
+    if (v4to6) {
+        void *from_ptr = &iph->saddr;
+        void *to_ptr   = &ip6h->saddr;
+
+        csum   = bpf_csum_diff(from_ptr, 2 * sizeof(__u32), to_ptr, 2 * sizeof(struct in6_addr), 0);
+        offset = L2_H_LEN(has_eth) + IP_H_LEN;
+        ip_type = ip6h->nexthdr;
+    } else {
+        void *from_ptr = &ip6h->saddr;
+        void *to_ptr   = &iph->saddr;
+
+        csum   = bpf_csum_diff(from_ptr, 2 * sizeof(struct in6_addr), to_ptr, 2 * sizeof(__u32), 0);
+        offset = L2_H_LEN(has_eth) + IP6_H_LEN;
+        ip_type = iph->protocol;
+
+        if (is_inner) {
+            offset = offset + ICMP6_H_LEN + IP6_H_LEN;
+        }
+    }
+
+    if (is_v6_fragment) {
+        offset += IP6_FRAG_H_LEN;
+    }
+
+    switch (ip_type) {
+    case IPPROTO_TCP:
+        offset += offsetof(struct tcphdr, check);
+        break;
+    case IPPROTO_UDP:
+        offset += offsetof(struct udphdr, check);
+        flags |= BPF_F_MARK_MANGLED_0;
+        break;
+    default:
+        return;
+    }
+
+    bpf_l4_csum_replace(skb, offset, 0, csum, flags);
+
+    if (csum_diff) {
+        *csum_diff = bpf_csum_diff((__be32 *) &csum, sizeof(csum), 0, 0, *csum_diff);
+    }
+}
+
+static __always_inline void
+update_icmp_checksum(struct __sk_buff     *skb,
+                     const struct ipv6hdr *ip6h,
+                     void                 *icmp_before,
+                     void                 *icmp_after,
+                     bool                  has_eth,
+                     bool                  v4to6,
+                     bool                  is_inner,
+                     __u32                 seed)
+{
+    struct icmpv6_pseudo ph = {.nh = IPPROTO_ICMPV6, .len = ip6h->payload_len};
+    __u16                h_before;
+    __u16                h_after;
+    __u16                offset;
+    __u32                csum;
+    __u32                u_before;
+    __u32                u_after;
+
+    __builtin_memcpy(&ph.saddr, &ip6h->saddr, sizeof(struct in6_addr));
+    __builtin_memcpy(&ph.daddr, &ip6h->daddr, sizeof(struct in6_addr));
+
+    /* Do checksum update in two passes: first compute the incremental
+     * checksum update of the ICMPv6 pseudo header, update the checksum
+     * using bpf_l4_csum_replace(), and then do a separate update for the
+     * ICMP type and code (which is two consecutive bytes, so cast them to
+     * u16). The bpf_csum_diff() helper can be used to compute the
+     * incremental update of the full block, whereas the
+     * bpf_l4_csum_replace() helper can do the two-byte diff and update by
+     * itself.
+     */
+    csum = bpf_csum_diff((__be32 *) &ph,
+                         v4to6 ? 0 : sizeof(ph),
+                         (__be32 *) &ph,
+                         v4to6 ? sizeof(ph) : 0,
+                         seed);
+
+    if (v4to6) {
+        offset = L2_H_LEN(has_eth) + IP_H_LEN + 2;
+    } else {
+        offset = L2_H_LEN(has_eth) + IP6_H_LEN + 2;
+        if (is_inner)
+            offset += ICMP6_H_LEN + IP6_H_LEN;
+    }
+
+    /* first two bytes of ICMP header, type and code */
+    h_before = *(__u16 *) icmp_before;
+    h_after  = *(__u16 *) icmp_after;
+
+    /* last four bytes of ICMP header, the data union */
+    u_before = *(__u32 *) (icmp_before + 4);
+    u_after  = *(__u32 *) (icmp_after + 4);
+
+    bpf_l4_csum_replace(skb, offset, 0, csum, BPF_F_PSEUDO_HDR);
+    bpf_l4_csum_replace(skb, offset, h_before, h_after, 2);
+
+    if (u_before != u_after)
+        bpf_l4_csum_replace(skb, offset, u_before, u_after, 4);
+}
+
+static __always_inline int
+rewrite_icmp(struct __sk_buff *skb, const struct ipv6hdr *ip6h, bool has_eth)
+{
+    void            *data_end = SKB_DATA_END(skb);
+    void            *data     = SKB_DATA(skb);
+    struct icmphdr   icmp_buf;  /* copy of the old ICMPv4 header */
+    struct icmp6hdr  icmp6_buf; /* buffer for the new ICMPv6 header */
+    struct icmphdr  *icmp;
+    struct icmp6hdr *icmp6;
+    __u32            mtu;
+
+    if (!ensure_header(&icmp, skb, &data, &data_end, L2_H_LEN(has_eth) + IP_H_LEN))
+        return -1;
+
+    icmp_buf  = *icmp;
+    icmp6     = (void *) icmp;
+    icmp6_buf = *icmp6;
+
+    /* These translations are defined in RFC6145 section 4.2 */
+    switch (icmp->type) {
+    case ICMP_ECHO:
+        icmp6_buf.icmp6_type = ICMPV6_ECHO_REQUEST;
+        break;
+    case ICMP_ECHOREPLY:
+        icmp6_buf.icmp6_type = ICMPV6_ECHO_REPLY;
+        break;
+    case ICMP_DEST_UNREACH:
+        icmp6_buf.icmp6_type = ICMPV6_DEST_UNREACH;
+        switch (icmp->code) {
+        case ICMP_NET_UNREACH:
+        case ICMP_HOST_UNREACH:
+        case ICMP_SR_FAILED:
+        case ICMP_NET_UNKNOWN:
+        case ICMP_HOST_UNKNOWN:
+        case ICMP_HOST_ISOLATED:
+        case ICMP_NET_UNR_TOS:
+        case ICMP_HOST_UNR_TOS:
+            icmp6_buf.icmp6_code = ICMPV6_NOROUTE;
+            break;
+        case ICMP_PROT_UNREACH:
+            icmp6_buf.icmp6_type    = ICMPV6_PARAMPROB;
+            icmp6_buf.icmp6_code    = ICMPV6_UNK_NEXTHDR;
+            icmp6_buf.icmp6_pointer = bpf_htonl(offsetof(struct ipv6hdr, nexthdr));
+            break;
+        case ICMP_PORT_UNREACH:
+            icmp6_buf.icmp6_code = ICMPV6_PORT_UNREACH;
+            break;
+        case ICMP_FRAG_NEEDED:
+            icmp6_buf.icmp6_type = ICMPV6_PKT_TOOBIG;
+            icmp6_buf.icmp6_code = 0;
+            mtu                  = bpf_ntohs(icmp->un.frag.mtu) + 20;
+            /* RFC6145 section 6, "second approach" - should not be
+             * necessary, but might as well do this
+             */
+            if (mtu < 1280)
+                mtu = 1280;
+            icmp6_buf.icmp6_mtu = bpf_htonl(mtu);
+            break;
+        case ICMP_NET_ANO:
+        case ICMP_HOST_ANO:
+        case ICMP_PKT_FILTERED:
+        case ICMP_PREC_CUTOFF:
+            icmp6_buf.icmp6_code = ICMPV6_ADM_PROHIBITED;
+            break;
+        default:
+            return -1;
+        }
+        break;
+    case ICMP_PARAMETERPROB:
+        if (icmp->code == 1)
+            return -1;
+        icmp6_buf.icmp6_type = ICMPV6_PARAMPROB;
+        icmp6_buf.icmp6_code = ICMPV6_HDR_FIELD;
+        /* The pointer field not defined in the Linux header. This
+         * translation is from Figure 3 of RFC6145.
+         */
+        switch (icmp->un.reserved[0]) {
+        case 0: /* version/IHL */
+            icmp6_buf.icmp6_pointer = 0;
+            break;
+        case 1: /* Type of Service */
+            icmp6_buf.icmp6_pointer = bpf_htonl(1);
+            break;
+        case 2: /* Total length */
+        case 3:
+            icmp6_buf.icmp6_pointer = bpf_htonl(4);
+            break;
+        case 8: /* Time to Live */
+            icmp6_buf.icmp6_pointer = bpf_htonl(7);
+            break;
+        case 9: /* Protocol */
+            icmp6_buf.icmp6_pointer = bpf_htonl(6);
+            break;
+        case 12: /* Source address */
+        case 13:
+        case 14:
+        case 15:
+            icmp6_buf.icmp6_pointer = bpf_htonl(8);
+            break;
+        case 16: /* Destination address */
+        case 17:
+        case 18:
+        case 19:
+            icmp6_buf.icmp6_pointer = bpf_htonl(24);
+            break;
+        default:
+            return -1;
+        }
+        break;
+    default:
+        return -1;
+    }
+
+    *icmp6 = icmp6_buf;
+    update_icmp_checksum(skb, ip6h, &icmp_buf, icmp6, has_eth, true, false, 0);
+
+    /* FIXME: also need to rewrite IP header embedded in ICMP error */
+
+    return 0;
+}
+
+/*
+  * Convert an IPv4 address to the corresponding "IPv4-Embedded IPv6 Address"
+  * according to RFC 6052 2.2.
+  *
+  *  +--+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
+  *  |PL| 0-------------32--40--48--56--64--72--80--88--96--104---------|
+  *  +--+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
+  *  |32|     prefix    |v4(32)         | u | suffix                    |
+  *  +--+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
+  *  |40|     prefix        |v4(24)     | u |(8)| suffix                |
+  *  +--+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
+  *  |48|     prefix            |v4(16) | u | (16)  | suffix            |
+  *  +--+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
+  *  |56|     prefix                |(8)| u |  v4(24)   | suffix        |
+  *  +--+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
+  *  |64|     prefix                    | u |   v4(32)      | suffix    |
+  *  +--+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
+  *  |96|     prefix                                    |    v4(32)     |
+  *  +--+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
+  *
+  */
+static __always_inline bool
+v4addr_to_v6(__be32 addr4, struct in6_addr *addr6, const struct in6_addr *pref64, int pref64_len)
+{
+    union {
+        __be32 a32;
+        __u8   a8[4];
+    } u;
+
+    u.a32 = addr4;
+
+    addr6->s6_addr32[0] = 0;
+    addr6->s6_addr32[1] = 0;
+    addr6->s6_addr32[2] = 0;
+    addr6->s6_addr32[3] = 0;
+
+    switch (pref64_len) {
+    case 96:
+        addr6->s6_addr32[0] = pref64->s6_addr32[0];
+        addr6->s6_addr32[1] = pref64->s6_addr32[1];
+        addr6->s6_addr32[2] = pref64->s6_addr32[2];
+        addr6->s6_addr32[3] = addr4;
+        break;
+    case 64:
+        addr6->s6_addr32[0] = pref64->s6_addr32[0];
+        addr6->s6_addr32[1] = pref64->s6_addr32[1];
+        addr6->s6_addr[9]   = u.a8[0];
+        addr6->s6_addr[10]  = u.a8[1];
+        addr6->s6_addr[11]  = u.a8[2];
+        addr6->s6_addr[12]  = u.a8[3];
+        break;
+    case 56:
+        addr6->s6_addr32[0] = pref64->s6_addr32[0];
+        addr6->s6_addr32[1] = pref64->s6_addr32[1];
+        addr6->s6_addr[7]   = u.a8[0];
+        addr6->s6_addr[9]   = u.a8[1];
+        addr6->s6_addr[10]  = u.a8[2];
+        addr6->s6_addr[11]  = u.a8[3];
+        break;
+    case 48:
+        addr6->s6_addr32[0] = pref64->s6_addr32[0];
+        addr6->s6_addr16[2] = pref64->s6_addr16[2];
+        addr6->s6_addr[6]   = u.a8[0];
+        addr6->s6_addr[7]   = u.a8[1];
+        addr6->s6_addr[9]   = u.a8[2];
+        addr6->s6_addr[10]  = u.a8[3];
+        break;
+    case 40:
+        addr6->s6_addr32[0] = pref64->s6_addr32[0];
+        addr6->s6_addr[4]   = pref64->s6_addr[4];
+        addr6->s6_addr[5]   = u.a8[0];
+        addr6->s6_addr[6]   = u.a8[1];
+        addr6->s6_addr[7]   = u.a8[2];
+        addr6->s6_addr[9]   = u.a8[3];
+        break;
+    case 32:
+        addr6->s6_addr32[0] = pref64->s6_addr32[0];
+        addr6->s6_addr32[1] = addr4;
+        break;
+    default:
+        return false;
+    }
+    return true;
+}
+
+/*
+  * Extract the IPv4 address @addr4 and the NAT64 prefix @pref64 from an IPv6 address,
+  * given the known prefix length @pref64_len. See the table above.
+  */
+static __always_inline bool
+v6addr_to_v4(const struct in6_addr *addr6, int pref64_len, __be32 *addr4, struct in6_addr *pref64)
+{
+    union {
+        __be32 a32;
+        __u8   a8[4];
+    } u;
+
+    pref64->s6_addr32[0] = 0;
+    pref64->s6_addr32[1] = 0;
+    pref64->s6_addr32[2] = 0;
+    pref64->s6_addr32[3] = 0;
+
+    switch (pref64_len) {
+    case 96:
+        u.a32 = addr6->s6_addr32[3];
+
+        pref64->s6_addr32[0] = addr6->s6_addr32[0];
+        pref64->s6_addr32[1] = addr6->s6_addr32[1];
+        pref64->s6_addr32[2] = addr6->s6_addr32[2];
+        break;
+    case 64:
+        u.a8[0] = addr6->s6_addr[9];
+        u.a8[1] = addr6->s6_addr[10];
+        u.a8[2] = addr6->s6_addr[11];
+        u.a8[3] = addr6->s6_addr[12];
+
+        pref64->s6_addr32[0] = addr6->s6_addr32[0];
+        pref64->s6_addr32[1] = addr6->s6_addr32[1];
+        break;
+    case 56:
+        u.a8[0] = addr6->s6_addr[7];
+        u.a8[1] = addr6->s6_addr[9];
+        u.a8[2] = addr6->s6_addr[10];
+        u.a8[3] = addr6->s6_addr[11];
+
+        pref64->s6_addr32[0] = addr6->s6_addr32[0];
+        pref64->s6_addr32[1] = addr6->s6_addr32[1];
+        pref64->s6_addr[7]   = 0;
+        break;
+    case 48:
+        u.a8[0] = addr6->s6_addr[6];
+        u.a8[1] = addr6->s6_addr[7];
+        u.a8[2] = addr6->s6_addr[9];
+        u.a8[3] = addr6->s6_addr[10];
+
+        pref64->s6_addr32[0] = addr6->s6_addr32[0];
+        pref64->s6_addr32[1] = addr6->s6_addr32[1];
+        pref64->s6_addr16[3] = 0;
+        break;
+    case 40:
+        u.a8[0] = addr6->s6_addr[5];
+        u.a8[1] = addr6->s6_addr[6];
+        u.a8[2] = addr6->s6_addr[7];
+        u.a8[3] = addr6->s6_addr[9];
+
+        pref64->s6_addr32[0] = addr6->s6_addr32[0];
+        pref64->s6_addr32[1] = addr6->s6_addr32[1];
+        pref64->s6_addr16[3] = 0;
+        pref64->s6_addr[5]   = 0;
+
+        break;
+    case 32:
+        u.a32 = addr6->s6_addr32[1];
+
+        pref64->s6_addr32[0] = addr6->s6_addr32[0];
+        break;
+    default:
+        return false;
+    }
+
+    *addr4 = u.a32;
+    return true;
+}
+
+/* ipv4 traffic in from application on this device, needs to be translated to v6 and sent to PLAT */
+static __always_inline int
+clat_handle_v4(struct __sk_buff *skb, bool has_eth)
+{
+    int             ret      = TC_ACT_OK;
+    void           *data_end = SKB_DATA_END(skb);
+    void           *data     = SKB_DATA(skb);
+    struct ipv6hdr *ip6h;
+    struct ipv6hdr  dst_hdr = {
+         .version = 6,
+    };
+    struct iphdr  *iph;
+    struct ethhdr *eth;
+
+    if (!ensure_header(&iph, skb, &data, &data_end, L2_H_LEN(has_eth)))
+        goto out;
+
+    if (has_eth) {
+        eth = data;
+        if (eth->h_proto != bpf_htons(ETH_P_IP))
+            goto out;
+    }
+
+    if (iph->version != 4)
+        goto out;
+
+    if (iph->saddr != config.local_v4.s_addr)
+        goto out;
+
+    /* At this point we know the packet needs translation. If we can't
+     * rewrite it, it should be dropped.
+     */
+    ret = TC_ACT_SHOT;
+
+    /* we don't bother dealing with IP options or fragmented packets. The
+     * latter are identified by the 'frag_off' field having a value (either
+     * the MF bit, or the fragment offset, or both). However, this field also
+     * contains the "don't fragment" (DF) bit, which we ignore, so mask that
+     * out. The DF is the second-most-significant bit (as bit 0 is
+     * reserved).
+     */
+
+    if (iph->ihl != 5 || (iph->frag_off & ~bpf_htons(1 << 14))) {
+        DBG("v4: pkt src/dst %pI4/%pI4 has IP options or is fragmented, dropping\n",
+            &iph->saddr,
+            &iph->daddr);
+        goto out;
+    }
+
+    if (!v4addr_to_v6(iph->daddr, &dst_hdr.daddr, &config.pref64, config.pref64_len))
+        goto out;
+
+    dst_hdr.saddr     = config.local_v6;
+    dst_hdr.nexthdr   = iph->protocol;
+    dst_hdr.hop_limit = iph->ttl;
+    /* weird definition in ipv6hdr */
+    dst_hdr.priority    = (iph->tos & 0x70) >> 4;
+    dst_hdr.flow_lbl[0] = iph->tos << 4;
+    dst_hdr.payload_len = bpf_htons(bpf_ntohs(iph->tot_len) - IP_H_LEN);
+
+    DBG("v4: outgoing pkt to dst %pI4 (%pI6c)\n", &iph->daddr, &dst_hdr.daddr);
+
+    switch (dst_hdr.nexthdr) {
+    case IPPROTO_ICMP:
+        if (rewrite_icmp(skb, &dst_hdr, has_eth))
+            goto out;
+        dst_hdr.nexthdr = IPPROTO_ICMPV6;
+        break;
+    case IPPROTO_TCP:
+    case IPPROTO_UDP:
+        update_l4_checksum(skb, &dst_hdr, iph, has_eth, true, false, false, NULL);
+        break;
+    default:
+        break;
+    }
+
+    if (bpf_skb_change_proto(skb, bpf_htons(ETH_P_IPV6), 0))
+        goto out;
+
+    data     = SKB_DATA(skb);
+    data_end = SKB_DATA_END(skb);
+
+    if (!ensure_header(&ip6h, skb, &data, &data_end, L2_H_LEN(has_eth)))
+        goto out;
+
+    if (has_eth) {
+        eth          = data;
+        eth->h_proto = bpf_htons(ETH_P_IPV6);
+    }
+
+    *ip6h = dst_hdr;
+
+    switch (dst_hdr.nexthdr) {
+    case IPPROTO_ICMPV6:
+        __sync_fetch_and_add(&stats.egress_icmp, 1);
+        break;
+    case IPPROTO_TCP:
+        __sync_fetch_and_add(&stats.egress_tcp, 1);
+        break;
+    case IPPROTO_UDP:
+        __sync_fetch_and_add(&stats.egress_udp, 1);
+        break;
+    default:
+        __sync_fetch_and_add(&stats.egress_other, 1);
+        break;
+    }
+
+    ret = bpf_redirect(skb->ifindex, 0);
+out:
+    if (ret == TC_ACT_SHOT)
+        __sync_fetch_and_add(&stats.egress_dropped, 1);
+    return ret;
+}
+
+static __always_inline __u16
+csum_fold_helper(__u32 csum)
+{
+    __u32 sum;
+    sum = (csum >> 16) + (csum & 0xffff);
+    sum += (sum >> 16);
+    return ~sum;
+}
+
+static __always_inline bool
+v6addr_equal(const struct in6_addr *a, const struct in6_addr *b)
+{
+    int i;
+
+    for (i = 0; i < 4; i++) {
+        if (a->s6_addr32[i] != b->s6_addr32[i])
+            return false;
+    }
+    return true;
+}
+
+static __always_inline void
+translate_ipv6_header(const struct ipv6hdr *ip6, struct iphdr *ip, __be32 saddr, __be32 daddr)
+{
+    *ip = (struct iphdr) {
+        .version  = 4,
+        .ihl      = 5,
+        .tos      = ip6->priority << 4 | (ip6->flow_lbl[0] >> 4),
+        .frag_off = bpf_htons(1 << 14),
+        .ttl      = ip6->hop_limit,
+        .protocol = ip6->nexthdr == IPPROTO_ICMPV6 ? IPPROTO_ICMP : ip6->nexthdr,
+        .saddr    = saddr,
+        .daddr    = daddr,
+        .tot_len  = bpf_htons(bpf_ntohs(ip6->payload_len) + IP_H_LEN),
+    };
+
+    ip->check = csum_fold_helper(bpf_csum_diff((__be32 *) ip, 0, (__be32 *) ip, IP_H_LEN, 0));
+}
+
+static __always_inline int
+translate_icmpv6_header(const struct icmp6hdr *icmp6, struct icmphdr *icmp)
+{
+    /* These translations are defined in RFC6145 section 5.2 */
+    switch (icmp6->icmp6_type) {
+    case ICMPV6_ECHO_REQUEST:
+        icmp->type = ICMP_ECHO;
+        break;
+    case ICMPV6_ECHO_REPLY:
+        icmp->type = ICMP_ECHOREPLY;
+        break;
+    case ICMPV6_DEST_UNREACH:
+        icmp->type = ICMP_DEST_UNREACH;
+        switch (icmp6->icmp6_code) {
+        case ICMPV6_NOROUTE:
+        case ICMPV6_NOT_NEIGHBOUR:
+        case ICMPV6_ADDR_UNREACH:
+            icmp->code = ICMP_HOST_UNREACH;
+            break;
+        case ICMPV6_ADM_PROHIBITED:
+            icmp->code = ICMP_HOST_ANO;
+            break;
+        case ICMPV6_PORT_UNREACH:
+            icmp->code = ICMP_PORT_UNREACH;
+            break;
+        default:
+            return -1;
+        }
+        break;
+    case ICMPV6_PKT_TOOBIG:
+    {
+        __u32 mtu;
+
+        icmp->type = ICMP_DEST_UNREACH;
+        icmp->code = ICMP_FRAG_NEEDED;
+
+        mtu = bpf_ntohl(icmp6->icmp6_mtu) - 20;
+        if (mtu > 0xffff)
+            return -1;
+        icmp->un.frag.mtu = bpf_htons(mtu);
+        break;
+    }
+    case ICMPV6_TIME_EXCEED:
+        icmp->type = ICMP_TIME_EXCEEDED;
+        break;
+    case ICMPV6_PARAMPROB:
+        switch (icmp6->icmp6_code) {
+        case 0:
+        {
+            __u32 ptr;
+
+            icmp->type = ICMP_PARAMETERPROB;
+            icmp->code = 0;
+
+            ptr = bpf_ntohl(icmp6->icmp6_pointer);
+            /* Figure 6 in RFC6145 - using if statements b/c of
+             * range at the bottom
+             */
+            if (ptr == 0 || ptr == 1)
+                icmp->un.reserved[0] = ptr;
+            else if (ptr == 4 || ptr == 5)
+                icmp->un.reserved[0] = 2;
+            else if (ptr == 6)
+                icmp->un.reserved[0] = 9;
+            else if (ptr == 7)
+                icmp->un.reserved[0] = 8;
+            else if (ptr >= 8 && ptr <= 23)
+                icmp->un.reserved[0] = 12;
+            else if (ptr >= 24 && ptr <= 39)
+                icmp->un.reserved[0] = 16;
+            else
+                return -1;
+            break;
+        }
+        case 1:
+            icmp->type = ICMP_DEST_UNREACH;
+            icmp->code = ICMP_PROT_UNREACH;
+            break;
+        default:
+            return -1;
+        }
+        break;
+    default:
+        return -1;
+    }
+
+    return 0;
+}
+
+static __always_inline int
+rewrite_icmpv6_inner(struct __sk_buff *skb, __u32 *csum_diff, bool has_eth)
+{
+    void            *data_end = SKB_DATA_END(skb);
+    void            *data     = SKB_DATA(skb);
+    struct icmphdr  *icmp;
+    struct icmp6hdr *icmp6;
+    struct icmphdr   icmp_buf;  /* buffer for the new ICMPv4 header */
+    struct icmp6hdr  icmp6_buf; /* copy of the old ICMPv6 header */
+
+    /*
+     * icmp6:                                                    v
+     * -------------------------------------------------------------------------
+     * | Ethernet | IPv6             | ICMPv6 | IPv6             | ICMPv6 | ...
+     * -------------------------------------------------------------------------
+     */
+
+    if (!ensure_header(&icmp6,
+                       skb,
+                       &data,
+                       &data_end,
+                       L2_H_LEN(has_eth) + 2 * IP6_H_LEN + ICMP6_H_LEN))
+        return -1;
+
+    icmp6_buf = *icmp6;
+    icmp      = (void *) icmp6;
+    icmp_buf  = *icmp;
+
+    if (translate_icmpv6_header(icmp6, &icmp_buf))
+        return -1;
+
+    *icmp = icmp_buf;
+    update_icmp_checksum(skb,
+                         (struct ipv6hdr *) (data + L2_H_LEN(has_eth)),
+                         &icmp6_buf,
+                         icmp,
+                         has_eth,
+                         false,
+                         true,
+                         0);
+
+    if (csum_diff) {
+        data_end = SKB_DATA_END(skb);
+        data     = SKB_DATA(skb);
+
+        if (!ensure_header(&icmp,
+                           skb,
+                           &data,
+                           &data_end,
+                           L2_H_LEN(has_eth) + 2 * IP6_H_LEN + ICMP6_H_LEN))
+            return -1;
+
+        /* Compute the checksum difference between the old ICMPv6 header and the new ICMPv4 one */
+        *csum_diff =
+            bpf_csum_diff((__be32 *) &icmp6_buf, ICMP6_H_LEN, (__be32 *) &icmp6_buf, 0, *csum_diff);
+        *csum_diff = bpf_csum_diff((__be32 *) icmp, 0, (__be32 *) icmp, ICMP_H_LEN, *csum_diff);
+    }
+    return 0;
+}
+
+static __always_inline int
+rewrite_ipv6_inner(struct __sk_buff *skb, struct iphdr *dst_hdr, __u32 *csum_diff, bool has_eth)
+{
+    void           *data_end = SKB_DATA_END(skb);
+    void           *data     = SKB_DATA(skb);
+    struct ipv6hdr *ip6h;
+    __be32          addr4;
+    struct in6_addr subnet_v6;
+
+    /*
+     * ip6h:                                  v
+     * ----------------------------------------------------------------
+     * | Ethernet | IPv6             | ICMPv6 | IPv6             | ...
+     * ----------------------------------------------------------------
+     */
+
+    if (!ensure_header(&ip6h, skb, &data, &data_end, L2_H_LEN(has_eth) + IP6_H_LEN + ICMP6_H_LEN))
+        return -1;
+
+    if (!v6addr_equal(&ip6h->saddr, &config.local_v6))
+        return -1;
+    if (!v6addr_to_v4(&ip6h->daddr, config.pref64_len, &addr4, &subnet_v6))
+        return -1;
+    if (!v6addr_equal(&subnet_v6, &config.pref64))
+        return -1;
+
+    translate_ipv6_header(ip6h, dst_hdr, config.local_v4.s_addr, addr4);
+
+    if (csum_diff) {
+        /* Checksum difference between the old IPv6 header and the new IPv4 one */
+        *csum_diff = bpf_csum_diff((__be32 *) ip6h, IP6_H_LEN, (__be32 *) ip6h, 0, *csum_diff);
+
+        *csum_diff = bpf_csum_diff((__be32 *) dst_hdr, 0, (__be32 *) dst_hdr, IP_H_LEN, *csum_diff);
+    }
+
+    switch (dst_hdr->protocol) {
+    case IPPROTO_ICMP:
+        if (rewrite_icmpv6_inner(skb, csum_diff, has_eth))
+            return -1;
+        break;
+    case IPPROTO_TCP:
+    case IPPROTO_UDP:
+        update_l4_checksum(skb, ip6h, dst_hdr, has_eth, false, true, false, csum_diff);
+        break;
+    default:
+        break;
+    }
+
+    return 0;
+}
+
+static __always_inline int
+rewrite_icmpv6(struct __sk_buff *skb, int *out_length_diff, bool has_eth)
+{
+    void            *data_end = SKB_DATA_END(skb);
+    void            *data     = SKB_DATA(skb);
+    struct iphdr    *ip;
+    struct icmp6hdr *icmp6;
+    struct icmphdr  *icmp;
+    struct icmphdr   icmp_buf;  /* buffer for the new ICMPv4 header */
+    struct icmp6hdr  icmp6_buf; /* copy of the old ICMPv6 header */
+    struct iphdr     ip_in_buf; /* buffer for the new inner IPv4 header */
+    __u32            csum_diff = 0;
+
+    /*
+     * icmp6:                        v
+     * ---------------------------------------------
+     * | Ethernet | IPv6             | ICMPv6 | ...
+     * ---------------------------------------------
+     */
+
+    if (!ensure_header(&icmp6, skb, &data, &data_end, L2_H_LEN(has_eth) + IP6_H_LEN))
+        return -1;
+
+    icmp6_buf = *icmp6;
+    icmp      = (void *) icmp6;
+    icmp_buf  = *icmp;
+
+    if (translate_icmpv6_header(icmp6, &icmp_buf))
+        return -1;
+
+    if (icmp6->icmp6_type >= 128) {
+        /* ICMPv6 non-error message: only translate the header */
+        *icmp = icmp_buf;
+        update_icmp_checksum(skb,
+                             (struct ipv6hdr *) (data + L2_H_LEN(has_eth)),
+                             &icmp6_buf,
+                             icmp,
+                             has_eth,
+                             false,
+                             false,
+                             0);
+        return 0;
+    }
+
+    /* ICMPv6 error messages: we need to rewrite the headers in the inner packet.
+     * Track in csum_diff the incremental changes to the checksum for the ICMPv4
+     * header. */
+
+    if (rewrite_ipv6_inner(skb, &ip_in_buf, &csum_diff, has_eth))
+        return -1;
+
+    /* The inner IP header shrinks from 40 (IPv6) to 20 (IPv4) bytes; we need to move
+     * the L4 header and payload. BPF programs don't have an easy way to move a variable
+     * amount of packet data; use bpf_skb_adjust_room() which can add or remove data
+     * inside a packet. It doesn't support arbitrary offsets, but we can use BPF_ADJ_ROOM_NET
+     * to remove the bytes just after the L3 header, and rewrite the ICMP and the inner
+     * IP headers.
+     */
+    if (bpf_skb_adjust_room(skb, (int) IP_H_LEN - (int) IP6_H_LEN, BPF_ADJ_ROOM_NET, 0))
+        return -1;
+
+    *out_length_diff = (int) IP_H_LEN - (int) IP6_H_LEN;
+
+    data_end = SKB_DATA_END(skb);
+    data     = SKB_DATA(skb);
+
+    if (!ensure_header(&ip, skb, &data, &data_end, L2_H_LEN(has_eth) + IP6_H_LEN + ICMP_H_LEN))
+        return -1;
+
+    icmp = data + L2_H_LEN(has_eth) + IP6_H_LEN;
+
+    /* Rewrite the ICMPv6 header with the translated ICMPv4 one */
+    *icmp = icmp_buf;
+    /* Rewrite the inner IPv6 header with the translated IPv4 one */
+    *ip = ip_in_buf;
+
+    /* Update the ICMPv4 checksum according to all the changes in headers */
+    update_icmp_checksum(skb,
+                         (struct ipv6hdr *) (data + L2_H_LEN(has_eth)),
+                         &icmp6_buf,
+                         icmp,
+                         has_eth,
+                         false,
+                         false,
+                         csum_diff);
+
+    return 0;
+}
+
+/* ipv6 traffic from the PLAT, to be translated into ipv4 and sent to an application */
+static __always_inline int
+clat_handle_v6(struct __sk_buff *skb, bool has_eth)
+{
+    int             ret      = TC_ACT_OK;
+    void           *data_end = SKB_DATA_END(skb);
+    void           *data     = SKB_DATA(skb);
+    struct ethhdr  *eth;
+    struct ipv6hdr *ip6h;
+    struct iphdr   *iph;
+    struct iphdr    dst_hdr;
+    struct in6_addr subnet_v6;
+    __be32          addr4;
+    int             length_diff = 0;
+    bool            fragmented  = false;
+
+    if (!ensure_header(&ip6h, skb, &data, &data_end, L2_H_LEN(has_eth)))
+        goto out;
+
+    if (has_eth) {
+        eth = data;
+        if (eth->h_proto != bpf_htons(ETH_P_IPV6))
+            goto out;
+    }
+
+    if (ip6h->version != 6)
+        goto out;
+
+    if (!v6addr_equal(&ip6h->daddr, &config.local_v6))
+        goto out;
+    if (!v6addr_to_v4(&ip6h->saddr, config.pref64_len, &addr4, &subnet_v6))
+        goto out;
+    if (!v6addr_equal(&subnet_v6, &config.pref64)) {
+        struct icmp6hdr *icmp6;
+
+        /* Follow draft-ietf-v6ops-icmpext-xlat-v6only-source-01:
+         *
+         * "Whenever a translator translates an ICMPv6 Destination Unreachable,
+         *  ICMPv6 Time Exceeded or ICMPv6 Packet Too Big ([RFC4443]) to the
+         *  corresponding ICMPv4 ([RFC0792]) message, and the IPv6 source
+         *  address in the outermost IPv6 header is untranslatable, the
+         *  translator SHOULD use the dummy IPv4 address (192.0.0.8) as the IPv4
+         *  source address for the translated packet."
+         */
+        if (ip6h->nexthdr != IPPROTO_ICMPV6)
+            goto out;
+
+        if (!ensure_header(&icmp6, skb, &data, &data_end, L2_H_LEN(has_eth) + IP6_H_LEN))
+            goto out;
+
+        ip6h = data + L2_H_LEN(has_eth);
+
+        if (icmp6->icmp6_type != ICMPV6_DEST_UNREACH && icmp6->icmp6_type != ICMPV6_TIME_EXCEED
+            && icmp6->icmp6_type != ICMPV6_PKT_TOOBIG)
+            goto out;
+
+        DBG("v6: icmpv6 type %u from native address %pI6c, translating src to dummy ipv4\n",
+            icmp6->icmp6_type,
+            &ip6h->saddr);
+
+        addr4 = __cpu_to_be32(INADDR_DUMMY);
+    }
+
+    /* At this point we know the packet needs translation. If we can't
+     * rewrite it, it should be dropped.
+     */
+    ret = TC_ACT_SHOT;
+
+    if (ip6h->nexthdr == IPPROTO_TCP || ip6h->nexthdr == IPPROTO_UDP
+        || ip6h->nexthdr == IPPROTO_ICMPV6) {
+        translate_ipv6_header(ip6h, &dst_hdr, addr4, config.local_v4.s_addr);
+        DBG("v6: incoming pkt from src %pI6c (%pI4)\n", &ip6h->saddr, &addr4);
+    } else if (ip6h->nexthdr == IPPROTO_FRAGMENT) {
+        struct ip6_frag *frag;
+        int              tot_len;
+        __u16            offset;
+
+        if (!ensure_header(&frag, skb, &data, &data_end, L2_H_LEN(has_eth) + IP6_H_LEN))
+            goto out;
+
+        ip6h = data + L2_H_LEN(has_eth);
+
+        /* Translate into an IPv4 fragmented packet, RFC 6145 5.1.1 */
+
+        tot_len = bpf_ntohs(ip6h->payload_len) + IP_H_LEN - IP6_FRAG_H_LEN;
+
+        offset = bpf_ntohs(frag->offset);
+        offset = ((offset & 1) << 13) | /* More Fragments flag */
+                 (offset >> 3);         /* Offset in 8-octet units */
+
+        dst_hdr = (struct iphdr) {
+            .version  = 4,
+            .ihl      = 5,
+            .id       = bpf_htons(bpf_ntohl(frag->identification) & 0xffff),
+            .tos      = ip6h->priority << 4 | (ip6h->flow_lbl[0] >> 4),
+            .frag_off = bpf_htons(offset),
+            .ttl      = ip6h->hop_limit,
+            .protocol = frag->nexthdr == IPPROTO_ICMPV6 ? IPPROTO_ICMP : frag->nexthdr,
+            .saddr    = addr4,
+            .daddr    = config.local_v4.s_addr,
+            .tot_len  = bpf_htons(tot_len),
+        };
+
+        dst_hdr.check = csum_fold_helper(
+            bpf_csum_diff((__be32 *) &dst_hdr, 0, (__be32 *) &dst_hdr, IP_H_LEN, 0));
+
+        fragmented = true;
+
+        DBG("v6: incoming fragmented pkt from src %pI6c (%pI4), id 0x%x\n",
+            &ip6h->saddr,
+            &addr4,
+            bpf_ntohs(dst_hdr.id));
+    } else {
+        DBG("v6: pkt src/dst %pI6c/%pI6c has nexthdr %u, dropping\n", &ip6h->saddr, &ip6h->daddr);
+        goto out;
+    }
+
+    switch (dst_hdr.protocol) {
+    case IPPROTO_ICMP:
+        /* We can't update the checksum of ICMP fragmented packets: ICMPv4 doesn't use
+         * a pseudo header, while the ICMPv6 pseudo-header includes the total payload
+         * length, which is not known when parsing the first fragment. This makes it
+         * impossible for a stateless translator to compute the checksum delta. TCP and
+         * UDP don't have this problem because both the v4 and v6 pseudo-headers include
+         * the total length. */
+        if (fragmented)
+            goto out;
+
+        if (rewrite_icmpv6(skb, &length_diff, has_eth))
+            goto out;
+        break;
+    case IPPROTO_TCP:
+    case IPPROTO_UDP:
+        /* Update the L4 headers only for non-fragmented packets or for the first
+         * fragment, which contains the L4 header. */
+        if (!fragmented || (bpf_ntohs(dst_hdr.frag_off) & 0x1FFF) == 0) {
+            update_l4_checksum(skb, ip6h, &dst_hdr, has_eth, false, false, fragmented, NULL);
+        }
+        break;
+    default:
+        break;
+    }
+
+    /* rewrite_icmpv6() can change the payload length when it rewrites the content of
+     * an ICMPv6 error packet. Update the length and the checksum. */
+    if (length_diff != 0) {
+        data     = SKB_DATA(skb);
+        data_end = SKB_DATA_END(skb);
+
+        if (!ensure_header(&ip6h, skb, &data, &data_end, L2_H_LEN(has_eth)))
+            goto out;
+
+        dst_hdr.tot_len = bpf_htons(bpf_ntohs(ip6h->payload_len) + length_diff + IP_H_LEN);
+
+        dst_hdr.check = 0;
+        dst_hdr.check = csum_fold_helper(
+            bpf_csum_diff((__be32 *) &dst_hdr, 0, (__be32 *) &dst_hdr, IP_H_LEN, 0));
+    }
+
+    if (bpf_skb_change_proto(skb, bpf_htons(ETH_P_IP), 0))
+        goto out;
+
+    if (fragmented) {
+        /* Remove the IPv6 fragment header */
+        if (bpf_skb_adjust_room(skb, -(__s32) IP6_FRAG_H_LEN, BPF_ADJ_ROOM_NET, 0))
+            goto out;
+    }
+
+    data     = SKB_DATA(skb);
+    data_end = SKB_DATA_END(skb);
+
+    if (!ensure_header(&iph, skb, &data, &data_end, L2_H_LEN(has_eth)))
+        goto out;
+
+    if (has_eth) {
+        eth          = data;
+        eth->h_proto = bpf_htons(ETH_P_IP);
+    }
+
+    *iph = dst_hdr;
+
+    if (fragmented)
+        __sync_fetch_and_add(&stats.ingress_fragment, 1);
+    switch (dst_hdr.protocol) {
+    case IPPROTO_ICMP:
+        __sync_fetch_and_add(&stats.ingress_icmp, 1);
+        break;
+    case IPPROTO_TCP:
+        __sync_fetch_and_add(&stats.ingress_tcp, 1);
+        break;
+    case IPPROTO_UDP:
+        __sync_fetch_and_add(&stats.ingress_udp, 1);
+        break;
+    default:
+        __sync_fetch_and_add(&stats.ingress_other, 1);
+        break;
+    }
+
+    ret = bpf_redirect(skb->ifindex, BPF_F_INGRESS);
+out:
+    if (ret == TC_ACT_SHOT)
+        __sync_fetch_and_add(&stats.ingress_dropped, 1);
+    return ret;
+}
+
+/* Use separate entry points for interfaces with and without an
+ * Ethernet header. Since all functions are now marked as inline,
+ * the compiler is able to replace the value of the parametric
+ * L2_H_LEN() macros with an immediate constant. This avoids
+ * pointer arithmetic which is forbidden because we don't run with
+ * CAP_PERFMON. The loader attaches the right program pair based
+ * on the interface type. */
+SEC("tcx/egress")
+int
+nm_clat_egress_eth(struct __sk_buff *skb)
+{
+    return clat_handle_v4(skb, true);
+}
+
+SEC("tcx/egress")
+int
+nm_clat_egress_rawip(struct __sk_buff *skb)
+{
+    return clat_handle_v4(skb, false);
+}
+
+SEC("tcx/ingress")
+int
+nm_clat_ingress_eth(struct __sk_buff *skb)
+{
+    return clat_handle_v6(skb, true);
+}
+
+SEC("tcx/ingress")
+int
+nm_clat_ingress_rawip(struct __sk_buff *skb)
+{
+    return clat_handle_v6(skb, false);
+}
diff --git a/src/core/bpf/clat.h b/src/core/bpf/clat.h
new file mode 100644
index 00000000..4a8adba5
--- /dev/null
+++ b/src/core/bpf/clat.h
@@ -0,0 +1,30 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+#ifndef __NAT64_H__
+#define __NAT64_H__
+
+#include <linux/in6.h>
+
+struct clat_config {
+    struct in6_addr local_v6;
+    struct in6_addr pref64;
+    struct in_addr  local_v4;
+    unsigned        pref64_len;
+};
+
+struct clat_stats {
+    /* egress: v4 to v6 */
+    __u64 egress_tcp;
+    __u64 egress_udp;
+    __u64 egress_icmp;
+    __u64 egress_other;
+    __u64 egress_dropped;
+    /* ingress: v6 to v4 */
+    __u64 ingress_tcp;
+    __u64 ingress_udp;
+    __u64 ingress_icmp;
+    __u64 ingress_other;
+    __u64 ingress_fragment;
+    __u64 ingress_dropped;
+};
+
+#endif
diff --git a/src/core/bpf/meson.build b/src/core/bpf/meson.build
new file mode 100644
index 00000000..39b978dd
--- /dev/null
+++ b/src/core/bpf/meson.build
@@ -0,0 +1,239 @@
+# SPDX-License-Identifier: LGPL-2.1+
+
+# Ripped from systemd: https://github.com/systemd/systemd/pull/20429
+
+if not enable_clat
+  subdir_done()
+endif
+
+bpf_compiler = get_option('bpf-compiler')
+clang_found = false
+clang_supports_bpf = false
+bpf_gcc_found = false
+bpftool_strip = false
+
+if bpf_compiler == 'clang' or bpf_compiler == 'auto'
+  # Support 'versioned' clang/llvm-strip binaries, as seen on Debian/Ubuntu
+  # (like clang-10/llvm-strip-10)
+  if meson.is_cross_build() or cc.get_id() != 'clang' or cc.cmd_array()[0].contains('afl-clang') or cc.cmd_array()[0].contains('hfuzz-clang')
+    r = find_program('clang',
+                     version : '>= 10.0.0')
+    clang_found = r.found()
+    if clang_found
+      if meson.version().version_compare('>= 0.55')
+        clang = r.full_path()
+      else
+        clang = r.path()
+      endif
+    endif
+  else
+    clang_found = true
+    clang = cc.cmd_array()
+  endif
+
+  if clang_found
+    # Check if 'clang -target bpf' is supported.
+    clang_supports_bpf = run_command(clang, '-target', 'bpf', '--print-supported-cpus', check : false).returncode() == 0
+  endif
+elif bpf_compiler == 'gcc' or bpf_compiler == 'auto'
+  bpf_gcc = find_program('bpf-gcc',
+                         'bpf-none-gcc',
+                         'bpf-unknown-none-gcc',
+                         version : '>= 13.1.0')
+  bpf_gcc_found = bpf_gcc.found()
+endif
+
+if bpf_compiler == 'auto'
+  if clang_supports_bpf and bpf_gcc_found
+    # Both supported, prefer the one matching our compiler:
+    if cc.get_id() == 'gcc'
+      bpf_compiler = 'gcc'
+    else
+      # Default to clang if we don't know this compiler
+      bpf_compiler = 'clang'
+    endif
+  elif clang_supports_bpf
+    bpf_compiler = 'clang'
+  elif bpf_gcc_found
+    bpf_compiler = 'clang'
+  endif
+endif
+
+if clang_supports_bpf or bpf_gcc_found
+  # Debian installs this in /usr/sbin/ which is not in $PATH.
+  # We check for 'bpftool' first, honouring $PATH, and in /usr/sbin/ for Debian.
+  # We use 'bpftool gen object' subcommand for bpftool strip, it was added by d80b2fcbe0a023619e0fc73112f2a02c2662f6ab (v5.13).
+  bpftool = find_program('bpftool',
+                         '/usr/sbin/bpftool',
+                         required : bpf_compiler == 'gcc',
+                         version : bpf_compiler == 'gcc' ? '>= 7.0.0' : '>= 5.13.0')
+
+  if bpftool.found()
+    bpftool_strip = true
+  elif bpf_compiler == 'clang'
+    # We require the 'bpftool gen skeleton' subcommand, it was added by 985ead416df39d6fe8e89580cc1db6aa273e0175 (v5.6).
+    bpftool = find_program('bpftool',
+                           '/usr/sbin/bpftool',
+                           required : true,
+                           version : '>= 5.6.0')
+  endif
+
+  # We use `llvm-strip` as a fallback if `bpftool gen object` strip support is not available.
+  if not bpftool_strip and bpftool.found() and clang_supports_bpf
+    if not meson.is_cross_build()
+      llvm_strip_bin = run_command(clang, '--print-prog-name', 'llvm-strip',
+                                   check : true).stdout().strip()
+    else
+      llvm_strip_bin = 'llvm-strip'
+    endif
+    llvm_strip = find_program(llvm_strip_bin,
+                              required : true,
+                              version : '>= 10.0.0')
+  endif
+else
+  error('clat support was enabled but couldn\'t find a suitable BPF compiler!')
+endif
+
+bpf_clang_flags = [
+  '-std=gnu17',
+  '-Wunused',
+  '-Wimplicit-fallthrough',
+  '-Wno-compare-distinct-pointer-types',
+  '-fno-stack-protector',
+  '-O2',
+  '-target',
+  'bpf',
+  '-g',
+  '-c',
+]
+
+bpf_gcc_flags = [
+  '-std=gnu17',
+  '-Wunused',
+  '-Wimplicit-fallthrough',
+  '-fno-stack-protector',
+  '-fno-ssa-phiopt',
+  '-O2',
+  '-mcpu=v3',
+  '-mco-re',
+  '-gbtf',
+  '-c',
+]
+
+clang_arch_flag = '-D__@0@__'.format(host_machine.cpu_family())
+
+libbpf_include_dir = dependency('libbpf').get_variable(pkgconfig : 'includedir')
+
+# Generate defines that are appropriate to tell the compiler what architecture
+# we're compiling for. By default we just map meson's cpu_family to __<cpu_family>__.
+# This dictionary contains the exceptions where this doesn't work.
+#
+# C.f. https://mesonbuild.com/Reference-tables.html#cpu-families
+# and src/basic/missing_syscall_def.h.
+cpu_arch_defines = {
+  'ppc'         : ['-D__powerpc__', '-D__TARGET_ARCH_powerpc'],
+  'ppc64'       : ['-D__powerpc64__', '-D__TARGET_ARCH_powerpc', '-D_CALL_ELF=2'],
+  'riscv32'     : ['-D__riscv', '-D__riscv_xlen=32', '-D__TARGET_ARCH_riscv'],
+  'riscv64'     : ['-D__riscv', '-D__riscv_xlen=64', '-D__TARGET_ARCH_riscv'],
+  'x86'         : ['-D__i386__', '-D__TARGET_ARCH_x86'],
+  's390x'       : ['-D__s390__', '-D__s390x__', '-D__TARGET_ARCH_s390'],
+
+  # For arm, assume hardware fp is available.
+  'arm'         : ['-D__arm__', '-D__ARM_PCS_VFP', '-D__TARGET_ARCH_arm'],
+  'loongarch64' : ['-D__loongarch__', '-D__loongarch_grlen=64', '-D__TARGET_ARCH_loongarch']
+}
+
+bpf_arch_flags = cpu_arch_defines.get(host_machine.cpu_family(),
+                                      ['-D__@0@__'.format(host_machine.cpu_family())])
+if bpf_compiler == 'gcc'
+  bpf_arch_flags += ['-m' + host_machine.endian() + '-endian']
+endif
+
+bpf_o_unstripped_cmd = []
+if bpf_compiler == 'clang'
+  bpf_o_unstripped_cmd += [
+    clang,
+    bpf_clang_flags,
+    bpf_arch_flags,
+  ]
+elif bpf_compiler == 'gcc'
+  bpf_o_unstripped_cmd += [
+    bpf_gcc,
+    bpf_gcc_flags,
+    bpf_arch_flags,
+  ]
+endif
+
+bpf_o_unstripped_cmd += ['-I.']
+
+if cc.get_id() == 'gcc' or meson.is_cross_build()
+  if cc.get_id() != 'gcc'
+    warning('Cross compiler is not gcc. Guessing the target triplet for bpf likely fails.')
+  endif
+  target_triplet_cmd = run_command(cc.cmd_array(), '-print-multiarch', check: false)
+else
+  # clang does not support -print-multiarch (D133170) and its -dump-machine
+  # does not match multiarch. Query gcc instead.
+  target_triplet_cmd = run_command('gcc', '-print-multiarch', check: false)
+endif
+
+if target_triplet_cmd.returncode() == 0
+  target_triplet = target_triplet_cmd.stdout().strip()
+  bpf_o_unstripped_cmd += [
+    '-isystem',
+    '/usr/include/@0@'.format(target_triplet)
+  ]
+endif
+
+bpf_o_unstripped_cmd += [
+  '-idirafter',
+  libbpf_include_dir,
+  '@INPUT@',
+  '-o',
+  '@OUTPUT@'
+]
+
+if bpftool_strip
+  bpf_o_cmd = [
+    bpftool,
+    'gen',
+    'object',
+    '@OUTPUT@',
+    '@INPUT@'
+  ]
+elif bpf_compiler == 'clang'
+  bpf_o_cmd = [
+    llvm_strip,
+    '-g',
+    '@INPUT@',
+    '-o',
+    '@OUTPUT@'
+  ]
+endif
+
+skel_h_cmd = [
+  bpftool,
+  'g',
+  's',
+  '@INPUT@'
+]
+
+clat_bpf_o_unstripped = custom_target(
+  'clat.bpf.unstripped.o',
+  input : 'clat.bpf.c',
+  output : 'clat.bpf.unstripped.o',
+  command : bpf_o_unstripped_cmd)
+
+clat_bpf_o = custom_target(
+  'clat.bpf.o',
+  input : clat_bpf_o_unstripped,
+  output : 'clat.bpf.o',
+  command : bpf_o_cmd)
+
+clat_skel_h = custom_target(
+  'clat.skel.h',
+  input : clat_bpf_o,
+  output : 'clat.skel.h',
+  command : skel_h_cmd,
+  capture : true)
+
diff --git a/src/core/devices/nm-device-bond.c b/src/core/devices/nm-device-bond.c
index 39e68e96..06d41a19 100644
--- a/src/core/devices/nm-device-bond.c
+++ b/src/core/devices/nm-device-bond.c
@@ -52,11 +52,12 @@
         NM_SETTING_BOND_OPTION_PACKETS_PER_SLAVE, NM_SETTING_BOND_OPTION_PRIMARY_RESELECT, \
         NM_SETTING_BOND_OPTION_RESEND_IGMP, NM_SETTING_BOND_OPTION_USE_CARRIER,            \
         NM_SETTING_BOND_OPTION_XMIT_HASH_POLICY, NM_SETTING_BOND_OPTION_NUM_GRAT_ARP,      \
-        NM_SETTING_BOND_OPTION_PEER_NOTIF_DELAY, NM_SETTING_BOND_OPTION_ARP_MISSED_MAX
+        NM_SETTING_BOND_OPTION_PEER_NOTIF_DELAY
 
-#define OPTIONS_REAPPLY_FULL                                     \
-    OPTIONS_REAPPLY_SUBSET, NM_SETTING_BOND_OPTION_ACTIVE_SLAVE, \
-        NM_SETTING_BOND_OPTION_ARP_IP_TARGET, NM_SETTING_BOND_OPTION_NS_IP6_TARGET
+#define OPTIONS_REAPPLY_FULL                                                        \
+    OPTIONS_REAPPLY_SUBSET, NM_SETTING_BOND_OPTION_ACTIVE_SLAVE,                    \
+        NM_SETTING_BOND_OPTION_ARP_IP_TARGET, NM_SETTING_BOND_OPTION_NS_IP6_TARGET, \
+        NM_SETTING_BOND_OPTION_ARP_MISSED_MAX
 
 /*****************************************************************************/
 
@@ -501,6 +502,8 @@ _platform_lnk_bond_init_from_setting(NMSettingBond *s_bond, NMPlatformLnkBond *p
     props->lp_interval_has      = props->lp_interval != 1;
     props->tlb_dynamic_lb_has   = NM_IN_SET(props->mode, NM_BOND_MODE_TLB, NM_BOND_MODE_ALB);
     props->lacp_active_has      = NM_IN_SET(props->mode, NM_BOND_MODE_8023AD);
+    props->arp_missed_max_has =
+        !NM_IN_SET(props->mode, NM_BOND_MODE_TLB, NM_BOND_MODE_ALB, NM_BOND_MODE_8023AD);
 }
 
 static void
@@ -907,6 +910,8 @@ reapply_connection(NMDevice *device, NMConnection *con_old, NMConnection *con_ne
     set_bond_arp_ip_targets(device, s_bond);
 
     set_bond_attrs_or_default(device, s_bond, NM_MAKE_STRV(OPTIONS_REAPPLY_SUBSET));
+    if (!NM_IN_SET(mode, NM_BOND_MODE_TLB, NM_BOND_MODE_ALB, NM_BOND_MODE_8023AD))
+        set_bond_attr_or_default(device, s_bond, NM_SETTING_BOND_OPTION_ARP_MISSED_MAX);
 
     _balance_slb_setup(self, con_new);
 }
diff --git a/src/core/devices/nm-device-ethernet.c b/src/core/devices/nm-device-ethernet.c
index 11f691de..b550b10a 100644
--- a/src/core/devices/nm-device-ethernet.c
+++ b/src/core/devices/nm-device-ethernet.c
@@ -1684,11 +1684,13 @@ complete_connection(NMDevice            *device,
 
         con_peer_name = nm_setting_veth_get_peer(s_veth);
         if (con_peer_name) {
-            nm_utils_error_set(error,
-                               NM_UTILS_ERROR_UNKNOWN,
-                               "mismatching veth peer \"%s\"",
-                               con_peer_name);
-            return FALSE;
+            if (!nm_streq(con_peer_name, peer_name)) {
+                nm_utils_error_set(error,
+                                   NM_UTILS_ERROR_UNKNOWN,
+                                   "mismatching veth peer \"%s\"",
+                                   con_peer_name);
+                return FALSE;
+            }
         } else
             g_object_set(s_veth, NM_SETTING_VETH_PEER, peer_name, NULL);
 
diff --git a/src/core/devices/nm-device-macvlan.c b/src/core/devices/nm-device-macvlan.c
index c5bcc91a..d3cbb663 100644
--- a/src/core/devices/nm-device-macvlan.c
+++ b/src/core/devices/nm-device-macvlan.c
@@ -468,7 +468,12 @@ static const NMDBusInterfaceInfoExtended interface_info_device_macvlan = {
             NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("NoPromisc",
                                                            "b",
                                                            NM_DEVICE_MACVLAN_NO_PROMISC),
-            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("Tab", "b", NM_DEVICE_MACVLAN_TAP), ), ),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("Tap", "b", NM_DEVICE_MACVLAN_TAP),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE(
+                "Tab",
+                "b",
+                NM_DEVICE_MACVLAN_TAP,
+                .annotations = NM_GDBUS_ANNOTATION_INFO_LIST_DEPRECATED(), ), ), ),
 };
 
 static void
diff --git a/src/core/devices/nm-device-veth.c b/src/core/devices/nm-device-veth.c
index c4b9e234..8be29fe9 100644
--- a/src/core/devices/nm-device-veth.c
+++ b/src/core/devices/nm-device-veth.c
@@ -53,7 +53,7 @@ update_properties(NMDevice *device)
     nm_device_parent_set_ifindex(device, peer_ifindex);
 
     peer = nm_device_parent_get_device(device);
-    if (peer && NM_IS_DEVICE_VETH(peer) && nm_device_parent_get_ifindex(peer) <= 0)
+    if (peer && NM_IS_DEVICE_VETH(peer) && !nm_device_parent_get_device(peer))
         update_properties(peer);
 }
 
diff --git a/src/core/devices/nm-device-vxlan.c b/src/core/devices/nm-device-vxlan.c
index 4058287c..87844e6f 100644
--- a/src/core/devices/nm-device-vxlan.c
+++ b/src/core/devices/nm-device-vxlan.c
@@ -176,14 +176,14 @@ create_and_realize(NMDevice              *device,
     if (str) {
         if (!nm_inet_parse_bin(AF_INET, str, NULL, &props.local)
             && !nm_inet_parse_bin(AF_INET6, str, NULL, &props.local6))
-            return FALSE;
+            return nm_assert_unreachable_val(FALSE);
     }
 
     str = nm_setting_vxlan_get_remote(s_vxlan);
     if (str) {
         if (!nm_inet_parse_bin(AF_INET, str, NULL, &props.group)
             && !nm_inet_parse_bin(AF_INET6, str, NULL, &props.group6))
-            return FALSE;
+            return nm_assert_unreachable_val(FALSE);
     }
 
     props.tos          = nm_setting_vxlan_get_tos(s_vxlan);
diff --git a/src/core/devices/nm-device.c b/src/core/devices/nm-device.c
index a0cf1ee6..e3457e34 100644
--- a/src/core/devices/nm-device.c
+++ b/src/core/devices/nm-device.c
@@ -805,7 +805,7 @@ typedef struct _NMDevicePrivate {
 
     GVariant *ports_variant; /* Array of port devices D-Bus path */
     char     *prop_ip_iface; /* IP interface D-Bus property */
-    GList    *ping_operations;
+    CList     ping_ops_lst_head;
     GSource  *ping_timeout;
 } NMDevicePrivate;
 
@@ -850,7 +850,6 @@ static const char *_activation_func_to_string(ActivationHandleFunc func);
 static void
 _set_state_full(NMDevice *self, NMDeviceState state, NMDeviceStateReason reason, gboolean quitting);
 static void queued_state_clear(NMDevice *device);
-static void ip_check_ping_watch_cb(GPid pid, int status, gpointer user_data);
 static void nm_device_start_ip_check(NMDevice *self);
 static void realize_start_setup(NMDevice             *self,
                                 const NMPlatformLink *plink,
@@ -907,10 +906,11 @@ static void concheck_update_state(NMDevice           *self,
 static void sriov_op_cb(GError *error, gpointer user_data);
 
 static void device_ifindex_changed_cb(NMManager *manager, NMDevice *device_changed, NMDevice *self);
-static gboolean device_link_changed(gpointer user_data);
-static gboolean _get_maybe_ipv6_disabled(NMDevice *self);
-static void     deactivate_ready(NMDevice *self, NMDeviceStateReason reason);
-static void     carrier_disconnected_action_cancel(NMDevice *self);
+static gboolean    device_link_changed(gpointer user_data);
+static gboolean    _get_maybe_ipv6_disabled(NMDevice *self);
+static void        deactivate_ready(NMDevice *self, NMDeviceStateReason reason);
+static void        carrier_disconnected_action_cancel(NMDevice *self);
+static const char *nm_device_get_effective_ip_config_method(NMDevice *self, int addr_family);
 
 /*****************************************************************************/
 
@@ -1294,7 +1294,7 @@ _prop_get_ipv6_dhcp_duid(NMDevice     *self,
             gint64  time;
             guint32 timestamp;
 
-#define EPOCH_DATETIME_THREE_YEARS (356 * 24 * 3600 * 3)
+#define EPOCH_DATETIME_THREE_YEARS (365 * 24 * 3600 * 3)
 
             /* We want a variable time between the host_id timestamp and three years
              * before. Let's compute the time (in seconds) from 0 to 3 years; then we'll
@@ -1524,6 +1524,50 @@ _prop_get_connection_dnssec(NMDevice *self, NMConnection *connection)
                                                        NM_SETTING_CONNECTION_DNSSEC_DEFAULT);
 }
 
+static NMSettingIp4ConfigClat
+_prop_get_ipv4_clat(NMDevice *self, gboolean do_log)
+{
+    NMSettingIP4Config    *s_ip4 = NULL;
+    NMSettingIp4ConfigClat clat;
+    const char            *method;
+
+    s_ip4 = nm_device_get_applied_setting(self, NM_TYPE_SETTING_IP4_CONFIG);
+    if (!s_ip4)
+        return NM_SETTING_IP4_CONFIG_CLAT_NO;
+
+    method = nm_device_get_effective_ip_config_method(self, AF_INET);
+    if (nm_streq(method, NM_SETTING_IP4_CONFIG_METHOD_DISABLED))
+        return NM_SETTING_IP4_CONFIG_CLAT_NO;
+
+    clat = nm_setting_ip4_config_get_clat(s_ip4);
+    if (clat == NM_SETTING_IP4_CONFIG_CLAT_DEFAULT) {
+        clat = nm_config_data_get_connection_default_int64(NM_CONFIG_GET_DATA,
+                                                           NM_CON_DEFAULT("ipv4.clat"),
+                                                           self,
+                                                           NM_SETTING_IP4_CONFIG_CLAT_NO,
+                                                           NM_SETTING_IP4_CONFIG_CLAT_FORCE,
+                                                           NM_SETTING_IP4_CONFIG_CLAT_NO);
+    }
+
+    if (clat == NM_SETTING_IP4_CONFIG_CLAT_AUTO
+        && !nm_streq(method, NM_SETTING_IP4_CONFIG_METHOD_AUTO)) {
+        /* clat=auto enables CLAT only with method=auto */
+        clat = NM_SETTING_IP4_CONFIG_CLAT_NO;
+    }
+
+    if (!HAVE_CLAT
+        && NM_IN_SET(clat, NM_SETTING_IP4_CONFIG_CLAT_AUTO, NM_SETTING_IP4_CONFIG_CLAT_FORCE)) {
+        if (do_log) {
+            _NMLOG(clat == NM_SETTING_IP4_CONFIG_CLAT_FORCE ? LOGL_WARN : LOGL_TRACE,
+                   LOGD_DEVICE,
+                   "CLAT will not work because it is disabled at build time");
+        }
+        clat = NM_SETTING_IP4_CONFIG_CLAT_NO;
+    }
+
+    return clat;
+}
+
 static NMMptcpFlags
 _prop_get_connection_mptcp_flags(NMDevice *self, NMConnection *connection)
 {
@@ -1922,26 +1966,43 @@ _prop_get_ipvx_may_fail_cached(NMDevice *self, int addr_family, NMTernary *cache
 }
 
 static gboolean
-_prop_get_ipv4_dhcp_ipv6_only_preferred(NMDevice *self)
+_prop_get_ipv4_dhcp_ipv6_only_preferred(NMDevice *self, gboolean *out_is_auto)
 {
     NMSettingIP4Config               *s_ip4;
     NMSettingIP4DhcpIpv6OnlyPreferred ipv6_only;
 
+    NM_SET_OUT(out_is_auto, FALSE);
+
     s_ip4 = nm_device_get_applied_setting(self, NM_TYPE_SETTING_IP4_CONFIG);
     if (!s_ip4)
         return FALSE;
 
     ipv6_only = nm_setting_ip4_config_get_dhcp_ipv6_only_preferred(s_ip4);
-    if (ipv6_only != NM_SETTING_IP4_DHCP_IPV6_ONLY_PREFERRED_DEFAULT)
-        return ipv6_only;
+    if (ipv6_only == NM_SETTING_IP4_DHCP_IPV6_ONLY_PREFERRED_DEFAULT) {
+        ipv6_only = nm_config_data_get_connection_default_int64(
+            NM_CONFIG_GET_DATA,
+            NM_CON_DEFAULT("ipv4.dhcp-ipv6-only-preferred"),
+            self,
+            NM_SETTING_IP4_DHCP_IPV6_ONLY_PREFERRED_NO,
+            NM_SETTING_IP4_DHCP_IPV6_ONLY_PREFERRED_AUTO,
+            NM_SETTING_IP4_DHCP_IPV6_ONLY_PREFERRED_AUTO);
+    }
 
-    return nm_config_data_get_connection_default_int64(
-        NM_CONFIG_GET_DATA,
-        NM_CON_DEFAULT("ipv4.dhcp-ipv6-only-preferred"),
-        self,
-        NM_SETTING_IP4_DHCP_IPV6_ONLY_PREFERRED_NO,
-        NM_SETTING_IP4_DHCP_IPV6_ONLY_PREFERRED_YES,
-        NM_SETTING_IP4_DHCP_IPV6_ONLY_PREFERRED_NO);
+    if (NM_IN_SET(ipv6_only,
+                  NM_SETTING_IP4_DHCP_IPV6_ONLY_PREFERRED_YES,
+                  NM_SETTING_IP4_DHCP_IPV6_ONLY_PREFERRED_NO))
+        return ipv6_only == NM_SETTING_IP4_DHCP_IPV6_ONLY_PREFERRED_YES;
+
+    /* auto */
+    NM_SET_OUT(out_is_auto, TRUE);
+
+    if (nm_streq0(nm_device_get_effective_ip_config_method(self, AF_INET6),
+                  NM_SETTING_IP6_CONFIG_METHOD_AUTO)
+        && _prop_get_ipv4_clat(self, FALSE) != NM_SETTING_IP4_CONFIG_CLAT_NO) {
+        return TRUE;
+    }
+
+    return FALSE;
 }
 
 /**
@@ -3623,6 +3684,7 @@ nm_device_create_l3_config_data_from_connection(NMDevice *self, NMConnection *co
 {
     NML3ConfigData *l3cd;
     int             ifindex;
+    gs_free char   *gw_warning = NULL;
 
     nm_assert(NM_IS_DEVICE(self));
     nm_assert(!connection || NM_IS_CONNECTION(connection));
@@ -3642,6 +3704,11 @@ nm_device_create_l3_config_data_from_connection(NMDevice *self, NMConnection *co
     nm_l3_config_data_set_dnssec(l3cd, _prop_get_connection_dnssec(self, connection));
     nm_l3_config_data_set_ip6_privacy(l3cd, _prop_get_ipv6_ip6_privacy(self, connection));
     nm_l3_config_data_set_mptcp_flags(l3cd, _prop_get_connection_mptcp_flags(self, connection));
+
+    gw_warning = nm_connection_get_unreachable_gateways_warning(connection, FALSE);
+    if (gw_warning)
+        _LOGW(LOGD_IP, "%s", gw_warning);
+
     return l3cd;
 }
 
@@ -4924,7 +4991,7 @@ _dev_l3_cfg_notify_cb(NML3Cfg *l3cfg, const NML3ConfigNotifyData *notify_data, N
         if (state >= NM_DEVICE_STATE_IP_CONFIG && state < NM_DEVICE_STATE_DEACTIVATING) {
             /* FIXME(l3cfg): MTU handling should be moved to l3cfg. */
             if (l3cd)
-                priv->ip6_mtu = nm_l3_config_data_get_ip6_mtu(l3cd);
+                priv->ip6_mtu = nm_l3_config_data_get_ip6_mtu_ra(l3cd);
             _commit_mtu(self);
         }
         _dev_ipll4_check_fallback(self, l3cd);
@@ -6345,6 +6412,14 @@ concheck_is_possible(NMDevice *self)
     if (priv->state == NM_DEVICE_STATE_UNKNOWN)
         return FALSE;
 
+    if (!nm_config_data_get_device_config_boolean_by_device(
+            NM_CONFIG_GET_DATA,
+            NM_CONFIG_KEYFILE_KEY_DEVICE_CHECK_CONNECTIVITY,
+            self,
+            TRUE,
+            TRUE))
+        return FALSE;
+
     return TRUE;
 }
 
@@ -6365,8 +6440,10 @@ concheck_periodic_schedule_do(NMDevice *self, int addr_family, gint64 now_ns)
         goto out;
     }
 
-    if (!concheck_is_possible(self))
+    if (!concheck_is_possible(self)) {
+        concheck_update_state(self, addr_family, NM_CONNECTIVITY_UNKNOWN, FALSE);
         goto out;
+    }
 
     nm_assert(now_ns > 0);
     nm_assert(priv->concheck_x[IS_IPv4].p_cur_interval > 0);
@@ -6589,7 +6666,11 @@ concheck_update_interval(NMDevice *self, int addr_family, gboolean check_now)
         concheck_periodic_schedule_do(self, addr_family, 0);
 
         /* also update the fake connectivity state. */
-        concheck_update_state(self, addr_family, NM_CONNECTIVITY_FAKE, TRUE);
+        if (concheck_is_possible(self))
+            concheck_update_state(self, addr_family, NM_CONNECTIVITY_FAKE, TRUE);
+        else
+            concheck_update_state(self, addr_family, NM_CONNECTIVITY_UNKNOWN, FALSE);
+
         return;
     }
 
@@ -6618,6 +6699,7 @@ concheck_update_state(NMDevice           *self,
     /* @state is a result of the connectivity check. We only expect a precise
      * number of possible values. */
     nm_assert(NM_IN_SET(state,
+                        NM_CONNECTIVITY_UNKNOWN,
                         NM_CONNECTIVITY_LIMITED,
                         NM_CONNECTIVITY_PORTAL,
                         NM_CONNECTIVITY_FULL,
@@ -6941,8 +7023,11 @@ nm_device_check_connectivity(NMDevice                    *self,
                              NMDeviceConnectivityCallback callback,
                              gpointer                     user_data)
 {
-    if (!concheck_is_possible(self))
+    if (!concheck_is_possible(self)) {
+        concheck_update_state(self, AF_INET, NM_CONNECTIVITY_UNKNOWN, FALSE);
+        concheck_update_state(self, AF_INET6, NM_CONNECTIVITY_UNKNOWN, FALSE);
         return NULL;
+    }
 
     concheck_periodic_schedule_set(self, addr_family, CONCHECK_SCHEDULE_CHECK_EXTERNAL);
     return concheck_start(self, addr_family, callback, user_data, FALSE);
@@ -7149,7 +7234,9 @@ nm_device_controller_release_port(NMDevice           *self,
 
     info = find_port_info(self, port);
 
-    if (info->port_state == PORT_STATE_ATTACHED)
+    if (!info)
+        port_state_str = "(not registered)";
+    else if (info->port_state == PORT_STATE_ATTACHED)
         port_state_str = "(attached)";
     else if (info->port_state == PORT_STATE_NOT_ATTACHED)
         port_state_str = "(not attached)";
@@ -7162,7 +7249,7 @@ nm_device_controller_release_port(NMDevice           *self,
           "controller: release one port " NM_HASH_OBFUSCATE_PTR_FMT "/%s %s%s",
           NM_HASH_OBFUSCATE_PTR(port),
           nm_device_get_iface(port),
-          !info ? "(not registered)" : port_state_str,
+          port_state_str,
           release_type == RELEASE_PORT_TYPE_CONFIG_FORCE
               ? " (force-configure)"
               : (release_type == RELEASE_PORT_TYPE_CONFIG ? " (configure)" : "(no-config)"));
@@ -7191,6 +7278,8 @@ nm_device_controller_release_port(NMDevice           *self,
         if (ret == NM_TERNARY_DEFAULT) {
             port_priv->port_detach_count++;
             port_priv->port_detach_reason = reason;
+        } else {
+            g_object_unref(port);
         }
     }
 
@@ -7798,6 +7887,12 @@ device_link_changed(gpointer user_data)
                                                NM_UNMANAGED_PLATFORM_INIT,
                                                NM_UNMAN_FLAG_OP_SET_MANAGED,
                                                nm_device_get_manage_reason_external(self));
+
+        /* Now that we got UDEV's announcement we need to check ignore-carrier again.
+         * This is because the permanent MAC might have been set or changed. If we don't
+         * recheck we would ignore rules matching by MAC address. */
+        priv->ignore_carrier =
+            nm_config_data_get_ignore_carrier_by_device(nm_config_get_data(nm_config_get()), self);
     }
 
     _dev_unmanaged_check_external_down(self, FALSE, FALSE);
@@ -8330,6 +8425,17 @@ config_changed(NMConfig           *config,
             && !nm_device_get_applied_setting(self, NM_TYPE_SETTING_SRIOV))
             device_init_static_sriov_num_vfs(self);
     }
+
+    if (NM_FLAGS_HAS(changes, NM_CONFIG_CHANGE_VALUES) && concheck_is_possible(self)) {
+        /* restart (periodic) connectivity checks if they were previously disabled */
+        if (!nm_config_data_get_device_config_boolean_by_device(
+                old_data,
+                NM_CONFIG_KEYFILE_KEY_DEVICE_CHECK_CONNECTIVITY,
+                self,
+                TRUE,
+                TRUE))
+            nm_device_check_connectivity_update_interval(self);
+    }
 }
 
 static void
@@ -8463,10 +8569,13 @@ realize_start_setup(NMDevice             *self,
     nm_device_update_initial_hw_address(self);
     nm_device_update_permanent_hw_address(self, FALSE);
 
-    /* Note: initial hardware address must be read before calling get_ignore_carrier() */
+    /* Note: initial hardware address must be read before calling get_ignore_carrier(). We'll
+     * need to recheck ignore_carrier again after UDEV's announcement, as the permanent MAC
+     * address may be set by UDEV. */
     config = nm_config_get();
     priv->ignore_carrier =
         nm_config_data_get_ignore_carrier_by_device(nm_config_get_data(config), self);
+
     if (!priv->config_changed_id) {
         priv->config_changed_id = g_signal_connect(config,
                                                    NM_CONFIG_SIGNAL_CONFIG_CHANGED,
@@ -11440,6 +11549,8 @@ _dev_ipmanual_start(NMDevice *self)
         if (_prop_get_ipvx_routed_dns(self, AF_INET6) == NM_SETTING_IP_CONFIG_ROUTED_DNS_YES) {
             nm_l3_config_data_set_routed_dns(l3cd, AF_INET6, TRUE);
         }
+
+        nm_l3_config_data_set_clat_config(l3cd, _prop_get_ipv4_clat(self, TRUE));
     }
 
     if (!l3cd) {
@@ -11748,8 +11859,9 @@ _dev_ipdhcpx_start(NMDevice *self, int addr_family)
         gboolean               hostname_is_fqdn;
         gboolean               send_client_id;
         guint8                 dscp;
-        gboolean               dscp_explicit  = FALSE;
-        gboolean               ipv6_only_pref = FALSE;
+        gboolean               dscp_explicit       = FALSE;
+        gboolean               ipv6_only_pref      = FALSE;
+        gboolean               ipv6_only_pref_auto = FALSE;
 
         client_id = _prop_get_ipv4_dhcp_client_id(self, connection, hwaddr, &send_client_id);
         dscp      = _prop_get_ipv4_dhcp_dscp(self, &dscp_explicit);
@@ -11768,13 +11880,15 @@ _dev_ipdhcpx_start(NMDevice *self, int addr_family)
             hostname         = nm_setting_ip_config_get_dhcp_hostname(s_ip);
         }
 
-        if (_prop_get_ipv4_dhcp_ipv6_only_preferred(self)) {
+        if (_prop_get_ipv4_dhcp_ipv6_only_preferred(self, &ipv6_only_pref_auto)) {
             if (nm_streq0(priv->ipv6_method, NM_SETTING_IP6_CONFIG_METHOD_DISABLED)) {
                 _LOGI_ipdhcp(
                     addr_family,
                     "not requesting the \"IPv6-only preferred\" option because IPv6 is disabled");
             } else {
-                _LOGD_ipdhcp(addr_family, "requesting the \"IPv6-only preferred\" option");
+                _LOGD_ipdhcp(addr_family,
+                             "requesting the \"IPv6-only preferred\" option (%s enabled)",
+                             ipv6_only_pref_auto ? "automatically" : "explicitly");
                 ipv6_only_pref = TRUE;
             }
         }
@@ -13585,13 +13699,21 @@ activate_stage3_ip_config(NMDevice *self)
                   nm_device_get_ip_iface(self));
     }
 
-    /* We currently will attach ports in the state change NM_DEVICE_STATE_IP_CONFIG above.
-     * Note that kernel changes the MTU of bond ports, so we want to commit the MTU
-     * afterwards!
+    /*
+     * Let's make sure MTU matches what is configured. The reason it's done at this
+     * precise location is twofold:
+     *
+     * (1) Attaching ports above might affect the MTU.
+     *
+     * We currently will attach ports in the state change NM_DEVICE_STATE_IP_CONFIG
+     * above. This might reset the MTU to something different from the bond controller
+     * and it might not be a working configuration. But it's what the user asked for.
+     *
+     * (2) When MTU is under 1280 IPv6 can not work.
      *
-     * This might reset the MTU to something different from the bond controller and
-     * it might not be a working configuration. But it's what the user asked for, so
-     * let's do it! */
+     * Kernel will not expose sysctls, create or accept addresses that are needed for IPv6
+     * configuration when the MTU is too small (under 1280).
+     */
     _commit_mtu(self);
 
     if (!nm_device_managed_type_is_external(self)
@@ -13600,12 +13722,6 @@ activate_stage3_ip_config(NMDevice *self)
             && !NM_IN_STRSET(priv->ipv6_method,
                              NM_SETTING_IP6_CONFIG_METHOD_DISABLED,
                              NM_SETTING_IP6_CONFIG_METHOD_IGNORE)) {
-            /* Ensure the MTU makes sense. If it was below 1280 the kernel would not
-             * expose any ipv6 sysctls or allow presence of any addresses on the interface,
-             * including LL, which * would make it impossible to autoconfigure MTU to a
-             * correct value. */
-            _commit_mtu(self);
-
             /* Any method past this point requires an IPv6LL address. Use NM-controlled
              * IPv6LL if this is not an assumed connection, since assumed connections
              * will already have IPv6 set up.
@@ -14583,6 +14699,12 @@ check_and_reapply_connection(NMDevice            *self,
 
         reactivate_proxy_config(self);
 
+        /* Reapply may have changed the per-device L3 merge flags (ignore-auto-dns,
+         * ignore-auto-routes, never-default). Re-register the active l3cds so the
+         * refreshed flags reach l3cfg even for sources that are not restarted on
+         * reapply (e.g. DHCPv6 when the NDisc DHCP level is unchanged). */
+        _dev_l3_register_l3cds(self, priv->l3cfg, TRUE, FALSE);
+
         nm_device_l3cfg_commit(
             self,
             NM_FLAGS_HAS(reapply_flags, NM_DEVICE_REAPPLY_FLAGS_PRESERVE_EXTERNAL_IP)
@@ -15588,35 +15710,15 @@ _dispatcher_complete_proceed_state(NMDispatcherCallId *call_id, gpointer user_da
 /*****************************************************************************/
 
 typedef struct {
-    NMLogDomain log_domain;
-    NMDevice   *device;
-    gboolean    ping_addresses_require_all;
-    GSource    *watch;
-    GPid        pid;
-    char       *binary;
-    char       *address;
-    guint       deadline;
-} PingOperation;
-
-static PingOperation *
-ping_operation_new(NMDevice   *self,
-                   NMLogDomain log_domain,
-                   const char *address,
-                   const char *ping_binary,
-                   guint       ping_timeout,
-                   gboolean    ip_ping_addresses_require_all)
-{
-    PingOperation *ping_op = g_new0(PingOperation, 1);
-
-    ping_op->device                     = self;
-    ping_op->log_domain                 = log_domain;
-    ping_op->address                    = g_strdup(address);
-    ping_op->binary                     = g_strdup(ping_binary);
-    ping_op->deadline                   = ping_timeout + 10;
-    ping_op->ping_addresses_require_all = ip_ping_addresses_require_all;
+    char         *addr_str;
+    NMIPAddrTyped addr_bin;
 
-    return ping_op;
-}
+    NMLogDomain   log_domain;
+    NMDevice     *device;
+    GCancellable *cancellable;
+    gboolean      require_all;
+    CList         ping_ops_lst;
+} PingOperation;
 
 static void
 ip_check_pre_up(NMDevice *self)
@@ -15640,188 +15742,154 @@ ip_check_pre_up(NMDevice *self)
 }
 
 static void
-cleanup_ping_operation(PingOperation *ping_op)
+ping_op_cleanup(PingOperation *ping_op)
 {
-    if (ping_op->watch) {
-        nm_clear_g_source_inst(&ping_op->watch);
-    }
-
-    if (ping_op->pid) {
-        nm_utils_kill_child_async(ping_op->pid,
-                                  SIGTERM,
-                                  ping_op->log_domain,
-                                  "ping",
-                                  1000,
-                                  NULL,
-                                  NULL);
-        ping_op->pid = 0;
-    }
-
-    nm_clear_g_free(&ping_op->binary);
-    nm_clear_g_free(&ping_op->address);
+    nm_clear_g_cancellable(&ping_op->cancellable);
+    nm_clear_g_free(&ping_op->addr_str);
+    c_list_unlink_stale(&ping_op->ping_ops_lst);
 
     g_free(ping_op);
 }
 
-static gboolean
-spawn_ping_for_operation(NMDevice *self, PingOperation *ping_op)
-{
-    gs_free char         *str_timeout = NULL;
-    gs_free char         *tmp_str     = NULL;
-    const char           *args[]      = {ping_op->binary,
-                                         "-I",
-                                         nm_device_get_ip_iface(self),
-                                         "-c",
-                                         "1",
-                                         "-w",
-                                         NULL,
-                                         ping_op->address,
-                                         NULL};
-    gs_free_error GError *error       = NULL;
-    gboolean              ret;
-
-    args[6] = str_timeout = g_strdup_printf("%u", ping_op->deadline);
-
-    tmp_str = g_strjoinv(" ", (char **) args);
-    _LOGD(ping_op->log_domain, "ping: running '%s'", tmp_str);
-
-    ret = g_spawn_async("/",
-                        (char **) args,
-                        NULL,
-                        G_SPAWN_DO_NOT_REAP_CHILD,
-                        NULL,
-                        NULL,
-                        &ping_op->pid,
-                        &error);
+static void
+ping_cleanup(NMDevice *self)
+{
+    NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self);
+    PingOperation   *ping_op;
 
-    if (ret) {
-        ping_op->watch = nm_g_child_watch_add_source(ping_op->pid, ip_check_ping_watch_cb, ping_op);
-    } else {
-        _LOGD(ping_op->log_domain, "ping: could not spawn %s: %s", ping_op->binary, error->message);
+    while ((ping_op = c_list_first_entry(&priv->ping_ops_lst_head, PingOperation, ping_ops_lst))) {
+        ping_op_cleanup(ping_op);
     }
 
-    return ret;
+    nm_clear_g_source_inst(&priv->ping_timeout);
 }
 
-static gboolean
-respawn_ping_cb(gpointer user_data)
+static void
+ping_host_cb(GObject *source, GAsyncResult *result, gpointer user_data)
 {
-    PingOperation   *ping_op = (PingOperation *) user_data;
-    NMDevice        *self    = ping_op->device;
-    NMDevicePrivate *priv    = NM_DEVICE_GET_PRIVATE(self);
+    NMDevice             *self;
+    NMDevicePrivate      *priv;
+    PingOperation        *ping_op = user_data;
+    gs_free_error GError *error   = NULL;
+    gboolean              success;
+    NMLogDomain           log_domain;
+    gs_free char         *addr_str = NULL;
+
+    success = nm_utils_ping_host_finish(result, &error);
+    if (nm_utils_error_is_cancelled(error))
+        return;
 
-    nm_clear_g_source_inst(&ping_op->watch);
+    self       = NM_DEVICE(ping_op->device);
+    priv       = NM_DEVICE_GET_PRIVATE(self);
+    log_domain = ping_op->log_domain;
+    addr_str   = g_steal_pointer(&ping_op->addr_str);
 
-    if (!spawn_ping_for_operation(self, ping_op)) {
-        priv->ping_operations = g_list_remove(priv->ping_operations, ping_op);
-        cleanup_ping_operation(ping_op);
+    if (!success) {
+        /* it should never fail because we set an infinite timeout */
+        nm_assert_not_reached();
+        return;
+    }
 
-        if (g_list_length(priv->ping_operations) == 0) {
-            ip_check_pre_up(self);
+    if (ping_op->require_all) {
+        ping_op_cleanup(ping_op);
+        if (!c_list_is_empty(&priv->ping_ops_lst_head)) {
+            _LOGD(log_domain,
+                  "ping: check on address %s succeeded, waiting for other addresses",
+                  addr_str);
+            return;
         }
     }
 
-    return FALSE;
+    _LOGD(log_domain, "ping: check on address %s succeeded, continuing the activation", addr_str);
+
+    ping_cleanup(self);
+    ip_check_pre_up(self);
 }
 
 static void
-ip_check_ping_watch_cb(GPid pid, int status, gpointer user_data)
+ping_operation_start(NMDevice      *self,
+                     const char    *addr_str,
+                     NMIPAddrTyped *addr_bin,
+                     gboolean       require_all)
 {
-    PingOperation   *ping_op = (PingOperation *) user_data;
-    NMDevice        *self    = ping_op->device;
-    NMDevicePrivate *priv    = NM_DEVICE_GET_PRIVATE(self);
-    gboolean         success = FALSE;
-
-    if (!ping_op->watch)
-        return;
-
-    nm_clear_g_source_inst(&ping_op->watch);
-    ping_op->pid = 0;
-
-    if (WIFEXITED(status)) {
-        if (WEXITSTATUS(status) == 0) {
-            _LOGD(ping_op->log_domain, "ping: ping succeeded on %s", ping_op->address);
-            success = TRUE;
-        } else {
-            _LOGD(ping_op->log_domain,
-                  "ping: ping failed with error code %d on %s",
-                  WEXITSTATUS(status),
-                  ping_op->address);
-        }
+    PingOperation   *ping_op;
+    NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self);
+    NMIPAddrTyped    addr_bin_local;
+    char             buf[NM_INET_ADDRSTRLEN];
+    int              addr_family;
+    int              ret;
+
+    /* Exactly one of the two must be set */
+    nm_assert(!!addr_str ^ !!addr_bin);
+
+    /* Derive the string address from the binary and vice versa */
+    if (addr_str) {
+        ret = nm_inet_parse_bin_full(AF_UNSPEC,
+                                     FALSE,
+                                     addr_str,
+                                     &addr_family,
+                                     &addr_bin_local.addr.addr_ptr);
+        nm_assert(ret);
+        addr_bin_local.addr_family = addr_family;
+        addr_bin                   = &addr_bin_local;
     } else {
-        _LOGD(ping_op->log_domain,
-              "ping: stopped unexpectedly with status %d on %s",
-              status,
-              ping_op->address);
-    }
-
-    if (success) {
-        if (ping_op->ping_addresses_require_all) {
-            priv->ping_operations = g_list_remove(priv->ping_operations, ping_op);
-            if (g_list_length(priv->ping_operations) == 0) {
-                _LOGD(ping_op->log_domain,
-                      "ping: ip-ping-addresses requires all, all ping checks on ip-ping-addresses "
-                      "succeeded");
-                if (priv->ping_timeout)
-                    nm_clear_g_source_inst(&priv->ping_timeout);
-                ip_check_pre_up(self);
-            }
-            cleanup_ping_operation(ping_op);
-        } else {
-            nm_assert(priv->ping_operations);
-
-            g_list_free_full(priv->ping_operations, (GDestroyNotify) cleanup_ping_operation);
-            priv->ping_operations = NULL;
+        nm_inet_ntop(addr_bin->addr_family, addr_bin->addr.addr_ptr, buf);
+        addr_str = buf;
+    }
+
+    /* When pinging the gateway, the caller must ensure that the IP configuration is ready.
+     * For the ip-ping-addresses property, a valid connection always has may-fail=no for
+     * the families of all the target addresses. Thus, at this point the IP configuration
+     * must also be ready. */
+    nm_assert(priv->ip_data_x[NM_IS_IPv4(addr_bin->addr_family)].state == NM_DEVICE_IP_STATE_READY);
+
+    ping_op  = g_new(PingOperation, 1);
+    *ping_op = (PingOperation) {
+        .device      = self,
+        .cancellable = g_cancellable_new(),
+        .require_all = require_all,
+        .addr_bin    = *addr_bin,
+        .addr_str    = g_strdup(addr_str),
+        .log_domain  = (addr_bin->addr_family == AF_INET) ? LOGD_IP4 : LOGD_IP6,
+    };
 
-            if (priv->ping_timeout)
-                nm_clear_g_source_inst(&priv->ping_timeout);
+    /* Start the asynchronous ping operation */
+    nm_utils_ping_host(ping_op->addr_bin,
+                       nm_device_get_ip_ifindex(self),
+                       0, /* try forever */
+                       ping_op->cancellable,
+                       ping_host_cb,
+                       ping_op);
 
-            _LOGD(ping_op->log_domain,
-                  "ping: ip-ping-addresses requires any, one ping check on ip-ping-addresses "
-                  "succeeded");
-            ip_check_pre_up(self);
-        }
-    } else {
-        /* If ping exited with an error it may have returned early,
-         * wait 1 second and restart it */
-        ping_op->watch = nm_g_timeout_add_seconds_source(1, respawn_ping_cb, ping_op);
-    }
+    c_list_link_tail(&priv->ping_ops_lst_head, &ping_op->ping_ops_lst);
 }
 
 static gboolean
 ip_check_ping_timeout_cb(gpointer user_data)
 {
-    NMDevice        *self = NM_DEVICE(user_data);
-    NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self);
-
-    _LOGW(LOGD_DEVICE, "ping timeout: unreachable gateway or ip-ping-addresses");
-
-    if (priv->ping_operations) {
-        g_list_free_full(priv->ping_operations, (GDestroyNotify) cleanup_ping_operation);
-        priv->ping_operations = NULL;
+    NMDevice                     *self = NM_DEVICE(user_data);
+    NMDevicePrivate              *priv = NM_DEVICE_GET_PRIVATE(self);
+    PingOperation                *ping_op;
+    nm_auto_free_gstring GString *str = NULL;
+
+    if (_LOGW_ENABLED(LOGD_DEVICE)) {
+        str = g_string_new("");
+        c_list_for_each_entry (ping_op, &priv->ping_ops_lst_head, ping_ops_lst) {
+            if (str->len != 0)
+                g_string_append(str, ", ");
+            g_string_append(str, ping_op->addr_str);
+        }
+        _LOGW(LOGD_DEVICE,
+              "ping: the following addresses were not reachable within the timeout: %s",
+              str->str);
     }
 
-    if (priv->ping_timeout)
-        nm_clear_g_source_inst(&priv->ping_timeout);
+    ping_cleanup(self);
     ip_check_pre_up(self);
 
     return FALSE;
 }
 
-static gboolean
-start_ping(NMDevice *self, PingOperation *ping_op)
-{
-    NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self);
-
-    if (spawn_ping_for_operation(self, ping_op)) {
-        priv->ping_operations = g_list_append(priv->ping_operations, ping_op);
-        return TRUE;
-    }
-
-    cleanup_ping_operation(ping_op);
-    return FALSE;
-}
-
 static void
 nm_device_start_ip_check(NMDevice *self)
 {
@@ -15830,17 +15898,13 @@ nm_device_start_ip_check(NMDevice *self)
     NMSettingConnection *s_con;
     guint                gw_ping_timeout = 0;
     guint                ip_ping_timeout = 0;
-    const char          *ping_binary     = NULL;
-    char                 buf[NM_INET_ADDRSTRLEN];
-    NMLogDomain          log_domain = LOGD_IP4;
-    gboolean             ip_ping_addresses_require_all;
-    gboolean             ping_started = FALSE;
+    gboolean             require_all;
+    NMIPAddrTyped        addr_bin = {};
 
     /* Shouldn't be any active ping here, since IP_CHECK happens after the
      * first IP method completes.  Any subsequently completing IP method doesn't
-     * get checked.
-     */
-    g_return_if_fail(priv->ping_operations == NULL);
+     * get checked. */
+    g_return_if_fail(c_list_is_empty(&priv->ping_ops_lst_head));
     g_return_if_fail(priv->ip_data_4.state == NM_DEVICE_IP_STATE_READY
                      || priv->ip_data_6.state == NM_DEVICE_IP_STATE_READY);
 
@@ -15849,100 +15913,71 @@ nm_device_start_ip_check(NMDevice *self)
 
     s_con = nm_connection_get_setting_connection(connection);
     g_assert(s_con);
-    gw_ping_timeout               = nm_setting_connection_get_gateway_ping_timeout(s_con);
-    ip_ping_addresses_require_all = _prop_get_connection_ip_ping_addresses_require_all(self, s_con);
-    ip_ping_timeout               = nm_setting_connection_get_ip_ping_timeout(s_con);
+    gw_ping_timeout = nm_setting_connection_get_gateway_ping_timeout(s_con);
+    ip_ping_timeout = nm_setting_connection_get_ip_ping_timeout(s_con);
+    require_all     = _prop_get_connection_ip_ping_addresses_require_all(self, s_con);
 
-    buf[0] = '\0';
-    if (gw_ping_timeout != 0 && ip_ping_timeout == 0) {
+    /* the timeouts are mutually exclusive */
+    nm_assert(gw_ping_timeout == 0 || ip_ping_timeout == 0);
+
+    if (gw_ping_timeout > 0) {
         const NMPObject      *gw;
         const NML3ConfigData *l3cd;
 
-        _LOGD(LOGD_DEVICE, "starting ping gateway...");
-
         l3cd = priv->l3cfg ? nm_l3cfg_get_combined_l3cd(priv->l3cfg, TRUE) : NULL;
         if (!l3cd) {
             /* pass */
         } else if (priv->ip_data_4.state == NM_DEVICE_IP_STATE_READY) {
             gw = nm_l3_config_data_get_best_default_route(l3cd, AF_INET);
             if (gw) {
-                nm_inet4_ntop(NMP_OBJECT_CAST_IP4_ROUTE(gw)->gateway, buf);
-                ping_binary = nm_utils_find_helper("ping", "/usr/bin/ping", NULL);
-                log_domain  = LOGD_IP4;
+                addr_bin.addr_family = AF_INET;
+                addr_bin.addr.addr4  = NMP_OBJECT_CAST_IP4_ROUTE(gw)->gateway;
             }
         } else if (priv->ip_data_6.state == NM_DEVICE_IP_STATE_READY) {
             gw = nm_l3_config_data_get_best_default_route(l3cd, AF_INET6);
             if (gw) {
-                nm_inet6_ntop(&NMP_OBJECT_CAST_IP6_ROUTE(gw)->gateway, buf);
-                ping_binary = nm_utils_find_helper("ping6", "/usr/bin/ping6", NULL);
-                log_domain  = LOGD_IP6;
+                addr_bin.addr_family = AF_INET6;
+                addr_bin.addr.addr6  = NMP_OBJECT_CAST_IP6_ROUTE(gw)->gateway;
             }
         }
-    }
-
-    if (buf[0]) {
-        PingOperation *ping_op = ping_operation_new(self,
-                                                    log_domain,
-                                                    buf,
-                                                    ping_binary,
-                                                    gw_ping_timeout,
-                                                    ip_ping_addresses_require_all);
-
-        if (start_ping(self, ping_op))
-            ping_started = TRUE;
-    }
 
-    if (gw_ping_timeout == 0 && ip_ping_timeout != 0) {
+        if (addr_bin.addr_family != AF_UNSPEC) {
+            _LOGD(LOGD_DEVICE,
+                  "starting ping on the IPv%c gateway with a %u seconds timeout",
+                  nm_utils_addr_family_to_char(addr_bin.addr_family),
+                  gw_ping_timeout);
+            ping_operation_start(self, NULL, &addr_bin, require_all);
+        }
+    } else if (ip_ping_timeout > 0) {
         const NML3ConfigData *l3cd;
+        GArray               *ip_ping_addresses;
+        const char *const    *strv;
         guint                 i;
-        GArray            *ip_ping_addresses = _nm_setting_connection_get_ip_ping_addresses(s_con);
-        const char *const *strv = nm_strvarray_get_strv_notempty(ip_ping_addresses, NULL);
 
-        _LOGD(LOGD_DEVICE, "starting ping ip addresses...");
+        ip_ping_addresses = _nm_setting_connection_get_ip_ping_addresses(s_con);
+        strv              = nm_strvarray_get_strv_notnull(ip_ping_addresses, NULL);
 
-        l3cd = priv->l3cfg ? nm_l3cfg_get_combined_l3cd(priv->l3cfg, TRUE) : NULL;
+        _LOGD(LOGD_DEVICE,
+              "starting ping on the ip-ping-addresses with a %u seconds timeout",
+              ip_ping_timeout);
 
+        l3cd = priv->l3cfg ? nm_l3cfg_get_combined_l3cd(priv->l3cfg, TRUE) : NULL;
         if (l3cd) {
             for (i = 0; strv[i]; i++) {
-                const char     *s = strv[i];
-                struct in_addr  ipv4_addr;
-                struct in6_addr ipv6_addr;
-
-                if (priv->ip_data_4.state == NM_DEVICE_IP_STATE_READY
-                    && inet_pton(AF_INET, (const char *) s, &ipv4_addr)) {
-                    ping_binary = nm_utils_find_helper("ping", "/usr/bin/ping", NULL);
-                    log_domain  = LOGD_IP4;
-                } else if (priv->ip_data_6.state == NM_DEVICE_IP_STATE_READY
-                           && inet_pton(AF_INET6, (const char *) s, &ipv6_addr)) {
-                    ping_binary = nm_utils_find_helper("ping6", "/usr/bin/ping6", NULL);
-                    log_domain  = LOGD_IP6;
-                } else
-                    continue;
-
-                if (s[0]) {
-                    PingOperation *ping_op = ping_operation_new(self,
-                                                                log_domain,
-                                                                s,
-                                                                ping_binary,
-                                                                ip_ping_timeout,
-                                                                ip_ping_addresses_require_all);
-
-                    if (start_ping(self, ping_op))
-                        ping_started = TRUE;
-                }
+                ping_operation_start(self, strv[i], NULL, require_all);
             }
         }
     }
 
-    if (ping_started) {
+    if (c_list_is_empty(&priv->ping_ops_lst_head)) {
+        /* No ping operation in progress, advance to pre-up */
+        ip_check_pre_up(self);
+    } else {
         priv->ping_timeout =
-            nm_g_timeout_add_seconds_source(gw_ping_timeout ? gw_ping_timeout : ip_ping_timeout,
+            nm_g_timeout_add_seconds_source(ip_ping_timeout > 0 ? ip_ping_timeout : gw_ping_timeout,
                                             ip_check_ping_timeout_cb,
                                             self);
     }
-    /* If no ping was started, just advance to pre_up. */
-    else
-        ip_check_pre_up(self);
 }
 
 /*****************************************************************************/
@@ -17401,17 +17436,11 @@ _cancel_activation(NMDevice *self)
 
     _dispatcher_cleanup(self);
 
-    if (priv->ping_operations) {
-        g_list_free_full(priv->ping_operations, (GDestroyNotify) cleanup_ping_operation);
-        priv->ping_operations = NULL;
-    }
-
-    if (priv->ping_timeout)
-        nm_clear_g_source_inst(&priv->ping_timeout);
-
     _dev_ip_state_cleanup(self, AF_INET, FALSE);
     _dev_ip_state_cleanup(self, AF_INET6, FALSE);
 
+    ping_cleanup(self);
+
     /* Break the activation chain */
     activation_source_clear(self);
 }
@@ -17968,6 +17997,14 @@ _set_state_full(NMDevice *self, NMDeviceState state, NMDeviceStateReason reason,
             nm_device_cleanup(self, reason, CLEANUP_TYPE_DECONFIGURE);
         }
         break;
+    case NM_DEVICE_STATE_DEACTIVATING:
+        /* When deactivating, certain devices are removed/disconnected after the
+         * STATE_CHANGED signal is sent and before the DHCP release packet
+         * can be sent. To ensure the release packet is sent, we cleanup DHCP
+         * before the signal is emitted*/
+        _dev_ipdhcpx_cleanup(self, AF_INET, TRUE, FALSE);
+        _dev_ipdhcpx_cleanup(self, AF_INET6, TRUE, FALSE);
+        break;
     case NM_DEVICE_STATE_DISCONNECTED:
         if (old_state > NM_DEVICE_STATE_DISCONNECTED) {
             /* Ensure devices that previously assumed a connection now have
@@ -18017,6 +18054,7 @@ _set_state_full(NMDevice *self, NMDeviceState state, NMDeviceStateReason reason,
                                (guint32) state,
                                (guint32) old_state,
                                (guint32) reason);
+
     g_signal_emit(self,
                   signals[STATE_CHANGED],
                   0,
@@ -18167,13 +18205,8 @@ _set_state_full(NMDevice *self, NMDeviceState state, NMDeviceStateReason reason,
         break;
     }
     case NM_DEVICE_STATE_SECONDARIES:
-        if (priv->ping_operations) {
-            g_list_free_full(priv->ping_operations, (GDestroyNotify) cleanup_ping_operation);
-            priv->ping_operations = NULL;
-        }
-        if (priv->ping_timeout)
-            nm_clear_g_source_inst(&priv->ping_timeout);
         _LOGD(LOGD_DEVICE, "device entered SECONDARIES state");
+        ping_cleanup(self);
         break;
     default:
         break;
@@ -19153,14 +19186,14 @@ nm_device_get_supplicant_timeout(NMDevice *self)
                                                        SUPPLICANT_DEFAULT_TIMEOUT);
 }
 
-gboolean
-nm_device_auth_retries_try_next(NMDevice *self)
+static int
+_device_get_auth_retries(NMDevice *self)
 {
     NMDevicePrivate     *priv;
     NMSettingConnection *s_con;
     int                  auth_retries;
 
-    g_return_val_if_fail(NM_IS_DEVICE(self), FALSE);
+    g_return_val_if_fail(NM_IS_DEVICE(self), 0);
 
     priv         = NM_DEVICE_GET_PRIVATE(self);
     auth_retries = priv->auth_retries;
@@ -19192,13 +19225,47 @@ nm_device_auth_retries_try_next(NMDevice *self)
         priv->auth_retries = auth_retries;
     }
 
+    return auth_retries;
+}
+
+gboolean
+nm_device_auth_retries_has_next(NMDevice *self)
+{
+    int auth_retries;
+
+    g_return_val_if_fail(NM_IS_DEVICE(self), FALSE);
+
+    auth_retries = _device_get_auth_retries(self);
+
+    if (auth_retries == NM_DEVICE_AUTH_RETRIES_INFINITY)
+        return TRUE;
+
+    if (auth_retries > 0)
+        return TRUE;
+
+    return FALSE;
+}
+
+gboolean
+nm_device_auth_retries_try_next(NMDevice *self)
+{
+    NMDevicePrivate *priv;
+    int              auth_retries;
+
+    g_return_val_if_fail(NM_IS_DEVICE(self), FALSE);
+
+    priv         = NM_DEVICE_GET_PRIVATE(self);
+    auth_retries = _device_get_auth_retries(self);
+
     if (auth_retries == NM_DEVICE_AUTH_RETRIES_INFINITY)
         return TRUE;
     if (auth_retries <= 0) {
         nm_assert(auth_retries == 0);
         return FALSE;
     }
+
     priv->auth_retries--;
+
     return TRUE;
 }
 
@@ -19838,6 +19905,8 @@ nm_device_init(NMDevice *self)
     priv->available_connections = g_hash_table_new_full(nm_direct_hash, NULL, g_object_unref, NULL);
     priv->ip6_saved_properties  = g_hash_table_new_full(nm_str_hash, g_str_equal, NULL, g_free);
 
+    c_list_init(&priv->ping_ops_lst_head);
+
     priv->managed_type_ = NM_DEVICE_MANAGED_TYPE_EXTERNAL;
     /* If networking is already disabled at boot, we want to manage all devices
      * after re-enabling networking; hence, the initial state is MANAGED. */
diff --git a/src/core/devices/nm-device.h b/src/core/devices/nm-device.h
index 2f287953..c8069d7c 100644
--- a/src/core/devices/nm-device.h
+++ b/src/core/devices/nm-device.h
@@ -791,6 +791,7 @@ void     nm_device_update_permanent_hw_address(NMDevice *self, gboolean force_fr
 void     nm_device_update_dynamic_ip_setup(NMDevice *self, const char *reason);
 guint    nm_device_get_supplicant_timeout(NMDevice *self);
 
+gboolean nm_device_auth_retries_has_next(NMDevice *self);
 gboolean nm_device_auth_retries_try_next(NMDevice *self);
 
 gboolean nm_device_hw_addr_get_cloned(NMDevice     *self,
diff --git a/src/core/devices/wifi/nm-device-iwd.c b/src/core/devices/wifi/nm-device-iwd.c
index fa6e2f9d..94b9a7d7 100644
--- a/src/core/devices/wifi/nm-device-iwd.c
+++ b/src/core/devices/wifi/nm-device-iwd.c
@@ -2270,6 +2270,37 @@ add_new:
     return NM_ACT_STAGE_RETURN_SUCCESS;
 }
 
+static void
+set_powersave(NMDevice *device)
+{
+    NMDeviceIwd               *self = NM_DEVICE_IWD(device);
+    NMSettingWireless         *s_wireless;
+    NMSettingWirelessPowersave val;
+
+    s_wireless = nm_device_get_applied_setting(device, NM_TYPE_SETTING_WIRELESS);
+
+    g_return_if_fail(s_wireless);
+
+    val = nm_setting_wireless_get_powersave(s_wireless);
+    if (val == NM_SETTING_WIRELESS_POWERSAVE_DEFAULT) {
+        val = nm_config_data_get_connection_default_int64(NM_CONFIG_GET_DATA,
+                                                          "wifi.powersave",
+                                                          device,
+                                                          NM_SETTING_WIRELESS_POWERSAVE_IGNORE,
+                                                          NM_SETTING_WIRELESS_POWERSAVE_ENABLE,
+                                                          NM_SETTING_WIRELESS_POWERSAVE_IGNORE);
+    }
+
+    _LOGT(LOGD_WIFI, "powersave is set to %u", (unsigned) val);
+
+    if (val == NM_SETTING_WIRELESS_POWERSAVE_IGNORE)
+        return;
+
+    nm_platform_wifi_set_powersave(nm_device_get_platform(device),
+                                   nm_device_get_ifindex(device),
+                                   val == NM_SETTING_WIRELESS_POWERSAVE_ENABLE);
+}
+
 static NMActStageReturn
 act_stage2_config(NMDevice *device, NMDeviceStateReason *out_failure_reason)
 {
@@ -2297,6 +2328,8 @@ act_stage2_config(NMDevice *device, NMDeviceStateReason *out_failure_reason)
             goto out_fail;
         }
 
+        set_powersave(device);
+
         /* With priv->iwd_autoconnect we have to let IWD handle retries for
          * infrastructure networks.  IWD will not necessarily retry the same
          * network after a failure but it will likely go into an autoconnect
diff --git a/src/core/devices/wifi/nm-device-wifi.c b/src/core/devices/wifi/nm-device-wifi.c
index b41ed5e1..b836c1e8 100644
--- a/src/core/devices/wifi/nm-device-wifi.c
+++ b/src/core/devices/wifi/nm-device-wifi.c
@@ -194,6 +194,9 @@ static void supplicant_iface_notify_p2p_available(NMSupplicantInterface *iface,
 static void supplicant_iface_notify_wpa_psk_mismatch_cb(NMSupplicantInterface *iface,
                                                         NMDeviceWifi          *self);
 
+static void supplicant_iface_notify_wpa_sae_mismatch_cb(NMSupplicantInterface *iface,
+                                                        NMDeviceWifi          *self);
+
 static void periodic_update(NMDeviceWifi *self);
 
 static void ap_add_remove(NMDeviceWifi *self,
@@ -631,6 +634,10 @@ supplicant_interface_acquire_cb(NMSupplicantManager         *supplicant_manager,
                      NM_SUPPLICANT_INTERFACE_PSK_MISMATCH,
                      G_CALLBACK(supplicant_iface_notify_wpa_psk_mismatch_cb),
                      self);
+    g_signal_connect(priv->sup_iface,
+                     NM_SUPPLICANT_INTERFACE_SAE_MISMATCH,
+                     G_CALLBACK(supplicant_iface_notify_wpa_sae_mismatch_cb),
+                     self);
 
     _scan_notify_is_scanning(self);
 
@@ -2191,21 +2198,16 @@ supplicant_iface_wps_credentials_cb(NMSupplicantInterface *iface,
 
     val_key = g_variant_lookup_value(credentials, "Key", G_VARIANT_TYPE_BYTESTRING);
     if (val_key) {
-        char psk[64];
+        char psk[65];
 
         array = g_variant_get_fixed_array(val_key, &psk_len, 1);
-        if (psk_len >= 8 && psk_len <= 63) {
-            memcpy(psk, array, psk_len);
-            psk[psk_len] = '\0';
-            if (g_utf8_validate(psk, psk_len, NULL)) {
-                secrets = g_variant_new_parsed("[{%s, [{%s, <%s>}]}]",
-                                               NM_SETTING_WIRELESS_SECURITY_SETTING_NAME,
-                                               NM_SETTING_WIRELESS_SECURITY_PSK,
-                                               psk);
-                g_variant_ref_sink(secrets);
-            }
-        }
-        if (!secrets)
+        if (nm_wifi_utils_wps_key_to_psk((const guint8 *) array, psk_len, &psk)) {
+            secrets = g_variant_new_parsed("[{%s, [{%s, <%s>}]}]",
+                                           NM_SETTING_WIRELESS_SECURITY_SETTING_NAME,
+                                           NM_SETTING_WIRELESS_SECURITY_PSK,
+                                           psk);
+            g_variant_ref_sink(secrets);
+        } else
             _LOGW(LOGD_DEVICE | LOGD_WIFI, "WPS: ignore invalid PSK");
     }
 
@@ -2244,6 +2246,26 @@ wps_timeout_cb(gpointer user_data)
     return G_SOURCE_REMOVE;
 }
 
+static gboolean
+wifi_connection_is_new(NMDeviceWifi *self)
+{
+    NMDevice             *device = NM_DEVICE(self);
+    NMActRequest         *req;
+    NMSettingsConnection *connection;
+    guint64               timestamp = 0;
+
+    req = nm_device_get_act_request(device);
+    g_return_val_if_fail(NM_IS_ACT_REQUEST(req), TRUE);
+
+    connection = nm_act_request_get_settings_connection(req);
+    g_return_val_if_fail(NM_IS_SETTINGS_CONNECTION(connection), TRUE);
+
+    if (nm_settings_connection_get_timestamp(connection, &timestamp) && timestamp != 0)
+        return FALSE;
+
+    return TRUE;
+}
+
 static void
 wifi_secrets_get_secrets(NMDeviceWifi                *self,
                          const char                  *setting_name,
@@ -2398,10 +2420,11 @@ handle_8021x_or_psk_auth_fail(NMDeviceWifi              *self,
                               NMSupplicantInterfaceState old_state,
                               int                        disconnect_reason)
 {
-    NMDevice     *device = NM_DEVICE(self);
-    NMActRequest *req;
-    const char   *setting_name = NULL;
-    gboolean      handled      = FALSE;
+    NMDevice                    *device = NM_DEVICE(self);
+    NMActRequest                *req;
+    const char                  *setting_name = NULL;
+    NMSecretAgentGetSecretsFlags secret_flags = NM_SECRET_AGENT_GET_SECRETS_FLAG_ALLOW_INTERACTION
+                                                | NM_SECRET_AGENT_GET_SECRETS_FLAG_REQUEST_NEW;
 
     g_return_val_if_fail(new_state == NM_SUPPLICANT_INTERFACE_STATE_DISCONNECTED, FALSE);
 
@@ -2411,8 +2434,7 @@ handle_8021x_or_psk_auth_fail(NMDeviceWifi              *self,
     req = nm_device_get_act_request(NM_DEVICE(self));
     g_return_val_if_fail(req != NULL, FALSE);
 
-    if (need_new_8021x_secrets(self, old_state, &setting_name)
-        || need_new_wpa_psk(self, old_state, disconnect_reason, &setting_name)) {
+    if (need_new_8021x_secrets(self, old_state, &setting_name)) {
         nm_act_request_clear_secrets(req);
 
         _LOGI(LOGD_DEVICE | LOGD_WIFI,
@@ -2422,14 +2444,54 @@ handle_8021x_or_psk_auth_fail(NMDeviceWifi              *self,
         nm_device_state_changed(device,
                                 NM_DEVICE_STATE_NEED_AUTH,
                                 NM_DEVICE_STATE_REASON_SUPPLICANT_DISCONNECT);
-        wifi_secrets_get_secrets(self,
-                                 setting_name,
-                                 NM_SECRET_AGENT_GET_SECRETS_FLAG_ALLOW_INTERACTION
-                                     | NM_SECRET_AGENT_GET_SECRETS_FLAG_REQUEST_NEW);
-        handled = TRUE;
+        wifi_secrets_get_secrets(self, setting_name, secret_flags);
+        return TRUE;
+    }
+
+    if (need_new_wpa_psk(self, old_state, disconnect_reason, &setting_name)) {
+        nm_act_request_clear_secrets(req);
+        cleanup_association_attempt(self, TRUE);
+
+        if (wifi_connection_is_new(self)) {
+            _LOGI(LOGD_DEVICE | LOGD_WIFI,
+                  "Activation: (wifi) new connection disconnected during association, asking for "
+                  "new key");
+            nm_device_state_changed(device,
+                                    NM_DEVICE_STATE_NEED_AUTH,
+                                    NM_DEVICE_STATE_REASON_SUPPLICANT_DISCONNECT);
+            wifi_secrets_get_secrets(self, setting_name, secret_flags);
+            return TRUE;
+        }
+
+        if (!nm_device_auth_retries_try_next(device)) {
+            nm_device_state_changed(device,
+                                    NM_DEVICE_STATE_FAILED,
+                                    NM_DEVICE_STATE_REASON_NO_SECRETS);
+            return TRUE;
+        }
+
+        if (nm_device_auth_retries_has_next(device)) {
+            secret_flags &= ~NM_SECRET_AGENT_GET_SECRETS_FLAG_REQUEST_NEW;
+            _LOGI(
+                LOGD_DEVICE | LOGD_WIFI,
+                "Activation: (wifi) disconnected during association, reauthenticating connection");
+        } else {
+            _LOGI(LOGD_DEVICE | LOGD_WIFI,
+                  "Activation: (wifi) disconnected during association, asking for new key");
+        }
+
+        nm_device_state_changed(device,
+                                NM_DEVICE_STATE_NEED_AUTH,
+                                NM_DEVICE_STATE_REASON_SUPPLICANT_DISCONNECT);
+        wifi_secrets_get_secrets(self, setting_name, secret_flags);
+
+        return TRUE;
     }
 
-    return handled;
+    _LOGI(LOGD_DEVICE | LOGD_WIFI,
+          "Activation: (wifi) disconnected during association, retrying connection");
+
+    return FALSE;
 }
 
 static gboolean
@@ -2861,6 +2923,12 @@ supplicant_iface_notify_wpa_psk_mismatch_cb(NMSupplicantInterface *iface, NMDevi
     if (nm_device_get_state(device) != NM_DEVICE_STATE_CONFIG)
         return;
 
+    if (!wifi_connection_is_new(self) && nm_device_auth_retries_has_next(device)) {
+        _LOGI(LOGD_DEVICE | LOGD_WIFI,
+              "Activation: (wifi) psk mismatch reported by supplicant, retrying connection");
+        return;
+    }
+
     _LOGI(LOGD_DEVICE | LOGD_WIFI,
           "Activation: (wifi) psk mismatch reported by supplicant, asking for new key");
 
@@ -2879,6 +2947,34 @@ supplicant_iface_notify_wpa_psk_mismatch_cb(NMSupplicantInterface *iface, NMDevi
                                  | NM_SECRET_AGENT_GET_SECRETS_FLAG_REQUEST_NEW);
 }
 
+static void
+supplicant_iface_notify_wpa_sae_mismatch_cb(NMSupplicantInterface *iface, NMDeviceWifi *self)
+{
+    NMDevice     *device = NM_DEVICE(self);
+    NMActRequest *req;
+    const char   *setting_name = NM_SETTING_WIRELESS_SECURITY_SETTING_NAME;
+
+    if (nm_device_get_state(device) != NM_DEVICE_STATE_CONFIG)
+        return;
+
+    _LOGI(LOGD_DEVICE | LOGD_WIFI,
+          "Activation: (wifi) SAE password mismatch reported by supplicant, asking for new key");
+
+    req = nm_device_get_act_request(NM_DEVICE(self));
+    g_return_if_fail(req != NULL);
+
+    nm_act_request_clear_secrets(req);
+
+    cleanup_association_attempt(self, TRUE);
+    nm_device_state_changed(device,
+                            NM_DEVICE_STATE_NEED_AUTH,
+                            NM_DEVICE_STATE_REASON_SUPPLICANT_DISCONNECT);
+    wifi_secrets_get_secrets(self,
+                             setting_name,
+                             NM_SECRET_AGENT_GET_SECRETS_FLAG_ALLOW_INTERACTION
+                                 | NM_SECRET_AGENT_GET_SECRETS_FLAG_REQUEST_NEW);
+}
+
 /*
  * supplicant_connection_timeout_cb
  *
@@ -3222,8 +3318,19 @@ act_stage1_prepare(NMDevice *device, NMDeviceStateReason *out_failure_reason)
 static void
 ensure_hotspot_frequency(NMDeviceWifi *self, NMSettingWireless *s_wifi, NMWifiAP *ap)
 {
-    guint32     a_freqs[]  = {5180, 5200, 5220, 5745, 5765, 5785, 5805, 0};
-    guint32     bg_freqs[] = {2412, 2437, 2462, 2472, 0};
+    guint32     freqs_a[]    = {5180, /* only U-NII-1 channels: non-DFS and available everywhere */
+                                5200,
+                                5220,
+                                5240,
+                                0};
+    guint32     freqs_bg[]   = {2412, 2437, 2462, 2472, 0};
+    guint32     freqs_6ghz[] = {5975, /* only U-NII-5 PSC channels, for better compatibility */
+                                6055,
+                                6135,
+                                6215,
+                                6295,
+                                6375,
+                                0};
     guint32    *rnd_freqs;
     guint       rnd_freqs_len;
     NMDevice   *device = NM_DEVICE(self);
@@ -3234,7 +3341,7 @@ ensure_hotspot_frequency(NMDeviceWifi *self, NMSettingWireless *s_wifi, NMWifiAP
     guint       l;
 
     nm_assert(ap);
-    nm_assert(NM_IN_STRSET(band, NULL, "a", "bg"));
+    nm_assert(NM_IN_STRSET(band, NULL, "a", "bg", "6GHz"));
 
     if (nm_wifi_ap_get_freq(ap))
         return;
@@ -3268,11 +3375,14 @@ ensure_hotspot_frequency(NMDeviceWifi *self, NMSettingWireless *s_wifi, NMWifiAP
     }
 
     if (nm_streq0(band, "a")) {
-        rnd_freqs     = a_freqs;
-        rnd_freqs_len = G_N_ELEMENTS(a_freqs) - 1;
+        rnd_freqs     = freqs_a;
+        rnd_freqs_len = G_N_ELEMENTS(freqs_a) - 1;
+    } else if (nm_streq0(band, "6GHz")) {
+        rnd_freqs     = freqs_6ghz;
+        rnd_freqs_len = G_N_ELEMENTS(freqs_6ghz) - 1;
     } else {
-        rnd_freqs     = bg_freqs;
-        rnd_freqs_len = G_N_ELEMENTS(bg_freqs) - 1;
+        rnd_freqs     = freqs_bg;
+        rnd_freqs_len = G_N_ELEMENTS(freqs_bg) - 1;
     }
 
     /* shuffle the frequencies (inplace). The idea is to choose
diff --git a/src/core/devices/wifi/nm-wifi-ap.c b/src/core/devices/wifi/nm-wifi-ap.c
index ceb954b7..3e758972 100644
--- a/src/core/devices/wifi/nm-wifi-ap.c
+++ b/src/core/devices/wifi/nm-wifi-ap.c
@@ -574,16 +574,6 @@ nm_wifi_ap_to_string(const NMWifiAP *self, char *str_buf, gulong buf_len, gint64
     return str_buf;
 }
 
-static guint
-freq_to_band(guint32 freq)
-{
-    if (freq >= 4915 && freq <= 5825)
-        return 5;
-    else if (freq >= 2412 && freq <= 2484)
-        return 2;
-    return 0;
-}
-
 gboolean
 nm_wifi_ap_check_compatible(NMWifiAP *self, NMConnection *connection)
 {
@@ -631,12 +621,12 @@ nm_wifi_ap_check_compatible(NMWifiAP *self, NMConnection *connection)
 
     band = nm_setting_wireless_get_band(s_wireless);
     if (band) {
-        guint ap_band = freq_to_band(priv->freq);
+        const char *ap_band = nm_wifi_freq_to_band_prop(priv->freq);
 
-        if (!strcmp(band, "a") && ap_band != 5)
-            return FALSE;
-        else if (!strcmp(band, "bg") && ap_band != 2)
+        if (!nm_streq(band, ap_band))
             return FALSE;
+
+        return TRUE;
     }
 
     channel = nm_setting_wireless_get_channel(s_wireless);
diff --git a/src/core/devices/wifi/nm-wifi-utils.c b/src/core/devices/wifi/nm-wifi-utils.c
index 332352ab..2ee4ec2e 100644
--- a/src/core/devices/wifi/nm-wifi-utils.c
+++ b/src/core/devices/wifi/nm-wifi-utils.c
@@ -639,7 +639,7 @@ nm_wifi_utils_complete_connection(GBytes       *ap_ssid,
                 chan_valid = FALSE;
             }
 
-            band = nm_utils_wifi_freq_to_band(ap_freq);
+            band = nm_wifi_freq_to_band_prop(ap_freq);
             if (band) {
                 g_object_set(s_wifi, NM_SETTING_WIRELESS_BAND, band, NULL);
             } else {
@@ -890,6 +890,28 @@ nm_wifi_utils_is_manf_default_ssid(GBytes *ssid)
     return FALSE;
 }
 
+/* Convert a WPS "Key" credential into a PSK string. The key is either an
+ * 8..63 character passphrase or a 64 character hexadecimal PSK. The actual
+ * WPA-PSK validity check is shared with nm_utils_wpa_psk_valid(). */
+gboolean
+nm_wifi_utils_wps_key_to_psk(const guint8 *key, gsize key_len, char (*out_psk)[65])
+{
+    if (key_len > 64)
+        return FALSE;
+    if (key_len < 64 && !g_utf8_validate((const char *) key, key_len, NULL))
+        return FALSE;
+
+    memcpy(*out_psk, key, key_len);
+    (*out_psk)[key_len] = '\0';
+
+    /* An embedded NUL would make nm_utils_wpa_psk_valid() see a truncated
+     * string, so reject it explicitly. */
+    if (strlen(*out_psk) != key_len)
+        return FALSE;
+
+    return nm_utils_wpa_psk_valid(*out_psk);
+}
+
 /* To be used for connections where the SSID has been validated before */
 gboolean
 nm_wifi_connection_get_iwd_ssid_and_security(NMConnection         *connection,
@@ -1929,3 +1951,19 @@ nm_wifi_utils_wfd_info_eq(const NMIwdWfdInfo *a, const NMIwdWfdInfo *b)
     return a->source == b->source && a->sink == b->sink && a->port == b->port
            && a->has_audio == b->has_audio && a->has_uibc == b->has_uibc && a->has_cp == b->has_cp;
 }
+
+const char *
+nm_wifi_freq_to_band_prop(guint32 freq)
+{
+    switch (nm_utils_wifi_freq_to_band(freq)) {
+    case NM_WIFI_BAND_2_4_GHZ:
+        return "bg";
+    case NM_WIFI_BAND_5_GHZ:
+        return "a";
+    case NM_WIFI_BAND_6_GHZ:
+        return "6GHz";
+    default:
+    case NM_WIFI_BAND_UNKNOWN:
+        return NULL;
+    }
+}
diff --git a/src/core/devices/wifi/nm-wifi-utils.h b/src/core/devices/wifi/nm-wifi-utils.h
index 1d46a900..30920396 100644
--- a/src/core/devices/wifi/nm-wifi-utils.h
+++ b/src/core/devices/wifi/nm-wifi-utils.h
@@ -42,6 +42,8 @@ gboolean nm_wifi_utils_complete_connection(GBytes       *ssid,
 
 gboolean nm_wifi_utils_is_manf_default_ssid(GBytes *ssid);
 
+gboolean nm_wifi_utils_wps_key_to_psk(const guint8 *key, gsize key_len, char (*out_psk)[65]);
+
 gboolean nm_wifi_connection_get_iwd_ssid_and_security(NMConnection         *connection,
                                                       char                **ssid,
                                                       NMIwdNetworkSecurity *security);
@@ -56,4 +58,6 @@ bool    nm_wifi_utils_parse_wfd_ies(GBytes *ies, NMIwdWfdInfo *out_wfd);
 GBytes *nm_wifi_utils_build_wfd_ies(const NMIwdWfdInfo *wfd);
 bool    nm_wifi_utils_wfd_info_eq(const NMIwdWfdInfo *a, const NMIwdWfdInfo *b);
 
+const char *nm_wifi_freq_to_band_prop(guint32 freq);
+
 #endif /* __NM_WIFI_UTILS_H__ */
diff --git a/src/core/devices/wifi/tests/test-devices-wifi.c b/src/core/devices/wifi/tests/test-devices-wifi.c
index a52696ea..b50e2626 100644
--- a/src/core/devices/wifi/tests/test-devices-wifi.c
+++ b/src/core/devices/wifi/tests/test-devices-wifi.c
@@ -1459,6 +1459,50 @@ test_ssids_options_to_ptrarray(void)
 
 /*****************************************************************************/
 
+static void
+test_wps_key_to_psk(void)
+{
+    static const struct {
+        const char *key;
+        gsize       key_len;
+        gboolean    valid;
+    } cases[] = {
+        {"12345678", 8, TRUE},
+        {"supersecret123", 14, TRUE},
+        {"caf\xc3\xa9_key", 9, TRUE},
+        {"123456789012345678901234567890123456789012345678901234567890123", 63, TRUE},
+        {"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", 64, TRUE},
+        {"0123456789ABCDEF0123456789abcdef0123456789abcdef0123456789abcdeF", 64, TRUE},
+        {"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdeg", 64, FALSE},
+        {"", 0, FALSE},
+        {"1234567", 7, FALSE},
+        {"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0", 65, FALSE},
+        {"abc\xff"
+         "def123",
+         10,
+         FALSE},
+        {"0123456789abcdef0123456789abcdef\0"
+         "23456789abcdef0123456789abcdef0",
+         64,
+         FALSE},
+    };
+    gsize i;
+
+    for (i = 0; i < G_N_ELEMENTS(cases); i++) {
+        char     psk[65];
+        gboolean ok;
+
+        ok = nm_wifi_utils_wps_key_to_psk((const guint8 *) cases[i].key, cases[i].key_len, &psk);
+        g_assert_cmpint(ok, ==, cases[i].valid);
+        if (ok) {
+            g_assert_cmpmem(psk, cases[i].key_len, cases[i].key, cases[i].key_len);
+            g_assert_cmpint(psk[cases[i].key_len], ==, '\0');
+        }
+    }
+}
+
+/*****************************************************************************/
+
 NMTST_DEFINE();
 
 int
@@ -1605,5 +1649,7 @@ main(int argc, char **argv)
 
     g_test_add_func("/wifi/ssids_options_to_ptrarray", test_ssids_options_to_ptrarray);
 
+    g_test_add_func("/wifi/wps_key_to_psk", test_wps_key_to_psk);
+
     return g_test_run();
 }
diff --git a/src/core/devices/wwan/nm-modem-manager.c b/src/core/devices/wwan/nm-modem-manager.c
index 4a89f38e..b8438e3f 100644
--- a/src/core/devices/wwan/nm-modem-manager.c
+++ b/src/core/devices/wwan/nm-modem-manager.c
@@ -258,16 +258,7 @@ modm_handle_name_owner_changed(MMManager *modem_manager, GParamSpec *pspec, NMMo
     /* Available! */
     g_free(name_owner);
 
-    /* Hack alert: GDBusObjectManagerClient won't signal neither 'object-added'
-     * nor 'object-removed' if it was created while there was no ModemManager in
-     * the bus. This hack avoids this issue until we get a GIO with the fix
-     * included... */
-    modm_clear_manager(self);
-    modm_ensure_manager(self);
-
-    /* Whenever GDBusObjectManagerClient is fixed, we can just do the following:
-     * modm_manager_available (self);
-     */
+    modm_manager_available(self);
 }
 
 static void
diff --git a/src/core/dhcp/README.next.md b/src/core/dhcp/README.next.md
index 88fa6683..1e61e75a 100644
--- a/src/core/dhcp/README.next.md
+++ b/src/core/dhcp/README.next.md
@@ -54,17 +54,14 @@ complexity out of `NMDevice`.
       mean that it gave up. It will keep retrying, it's just that there is little
       hope of getting a new lease. This happens, when you try to run DHCP on a Layer3
       link (WireGuard). There is little hope to succeed, but `NMDhcpClient`
-      (theoretically) will retry and may recover from this. Another example is when
-      we fail to start dhclient because it's not installed. In that case, we are not
-      optimistic to recover, however `NMDhcpDhclient` will retry (with backoff
-      timeout) and might still recover from this. For most cases, `NMDevice` will
+      (theoretically) will retry and may recover from this. For most cases, `NMDevice` will
       treat the no-lease cases the same, but in case of "bad" it might give up
       earlier.
 
 When a lease expires, that does not necessarily mean that we are now in a bad
 state. It might mean that the DHCP server is temporarily down, but we might
 recover from that easily. "bad" really means, something is wrong on our side
-which prevents us from getting a lease. Also, imagine `dhclient` dies (we would
+which prevents us from getting a lease. Also, imagine the DHCP client dies (we would
 try to restart, but assume that fails too), but we still have a valid lease,
 then possibly `NMDhcpClient` should still pretend all is good and we still have
 a lease until it expires. It may be we can recover before that happens. The
@@ -88,8 +85,7 @@ optionally does ACD first, then configures the IP address first and calls
 different lease). With this, the above state "has a lease" has actually three
 flavors: "has a lease but not yet ACD probed" and "has a lease but
 accepted/declined" (but `NM_DHCP_CLIENT_SIGNAL_STATE_CHANGED` gets only emitted
-when we get the lease, not when we accept/decline it). With `dhclient`, when we
-receive a lease, it means  "has a lease but accepted" right away.
+when we get the lease, not when we accept/decline it).
 
 - for IPv6 prefix delegation, there is also `needed_prefixes` and
   `NM_DHCP_CLIENT_NOTIFY_TYPE_PREFIX_DELEGATED`. Currently `needed_prefixes` needs
diff --git a/src/core/dhcp/nm-dhcp-client.c b/src/core/dhcp/nm-dhcp-client.c
index 18ad4024..c17e5544 100644
--- a/src/core/dhcp/nm-dhcp-client.c
+++ b/src/core/dhcp/nm-dhcp-client.c
@@ -266,9 +266,8 @@ nm_dhcp_client_create_options_dict(NMDhcpClient *self, gboolean static_keys)
         guint option = IS_IPv4 ? NM_DHCP_OPTION_DHCP4_CLIENT_ID : NM_DHCP_OPTION_DHCP6_CLIENT_ID;
         gs_free char *str = nm_dhcp_utils_duid_to_string(effective_client_id);
 
-        /* Note that for the nm-dhcp-helper based plugins (dhclient), the plugin
-         * may send the used client-id/DUID via the environment variables and
-         * overwrite them yet again. */
+        /* Note that nm-dhcp-helper based plugins may send the used client-id/DUID
+         * via the environment variables and overwrite them yet again. */
 
         nm_dhcp_option_take_option(options,
                                    static_keys,
@@ -786,13 +785,12 @@ _nm_dhcp_client_notify(NMDhcpClient         *self,
                        NMDhcpClientEventType client_event_type,
                        const NML3ConfigData *l3cd)
 {
-    NMDhcpClientPrivate                     *priv = NM_DHCP_CLIENT_GET_PRIVATE(self);
-    GHashTable                              *options;
-    gboolean                                 l3cd_changed;
-    NMOptionBool                             acd_state;
-    const int                                IS_IPv4     = NM_IS_IPv4(priv->config.addr_family);
-    nm_auto_unref_l3cd const NML3ConfigData *l3cd_merged = NULL;
-    char                                     sbuf1[NM_HASH_OBFUSCATE_PTR_STR_BUF_SIZE];
+    NMDhcpClientPrivate *priv = NM_DHCP_CLIENT_GET_PRIVATE(self);
+    GHashTable          *options;
+    gboolean             l3cd_changed;
+    NMOptionBool         acd_state;
+    const int            IS_IPv4 = NM_IS_IPv4(priv->config.addr_family);
+    char                 sbuf1[NM_HASH_OBFUSCATE_PTR_STR_BUF_SIZE];
 
     nm_assert(NM_IN_SET(client_event_type,
                         NM_DHCP_CLIENT_EVENT_TYPE_UNSPECIFIED,
@@ -825,16 +823,6 @@ _nm_dhcp_client_notify(NMDhcpClient         *self,
     if (client_event_type >= NM_DHCP_CLIENT_EVENT_TYPE_TIMEOUT)
         watch_cleanup(self);
 
-    if (!IS_IPv4 && l3cd) {
-        /* nm_dhcp_utils_merge_new_dhcp6_lease() relies on "life_starts" option
-         * for merging, which is only set by dhclient. Internal client never sets that,
-         * but it supports multiple IP addresses per lease. */
-        if (nm_dhcp_utils_merge_new_dhcp6_lease(priv->l3cd_next, l3cd, &l3cd_merged)) {
-            _LOGD("lease merged with existing one");
-            l3cd = nm_l3_config_data_seal(l3cd_merged);
-        }
-    }
-
     if (l3cd) {
         nm_clear_g_source_inst(&priv->no_lease_timeout_source);
     } else
@@ -1460,7 +1448,9 @@ nm_dhcp_client_schedule_ipv6_only_restart(NMDhcpClient *self, guint timeout)
     nm_assert(!priv->is_stopped);
 
     timeout = NM_MAX(priv->v4.ipv6_only_min_wait, timeout);
-    _LOGI("received option \"ipv6-only-preferred\": stopping DHCPv4 for %u seconds", timeout);
+    _LOGI("received option \"ipv6-only-preferred\": stopping DHCPv4 for %u seconds. Set "
+          "ipv4.dhcp-ipv6-only-preferred=no to force the use of IPv4 on this IPv6-mostly network",
+          timeout);
 
     nm_dhcp_client_stop(self, FALSE);
     nm_clear_g_source_inst(&priv->no_lease_timeout_source);
@@ -1690,21 +1680,14 @@ maybe_add_option(NMDhcpClient *self, GHashTable *hash, const char *key, GVariant
 
     g_hash_table_insert(hash, g_strdup(key), str_value);
 
-    /* dhclient has no special labels for private dhcp options: it uses "unknown_xyz"
-     * labels for that. We need to identify those to alias them to our "private_xyz"
-     * format unused in the internal dchp plugins.
-     */
+    /* "unknown_xyz" labels are aliased to our "private_xyz" format. */
     if ((priv_opt_num = label_is_unknown_xyz(key)) > 0) {
         gs_free guint8 *check_val = NULL;
         char           *hex_str   = NULL;
         gsize           len;
 
-        /* dhclient passes values from dhcp private options in its own "string" format:
-         * if the raw values are printable as ascii strings, it will pass the string
-         * representation; if the values are not printable as an ascii string, it will
-         * pass a string displaying the hex values (hex string). Try to enforce passing
-         * always an hex string, converting string representation if needed.
-         */
+        /* Private options may arrive as printable ascii strings or as hex strings.
+         * Normalize to always use hex string format. */
         check_val = nm_utils_hexstr2bin_alloc(str_value, FALSE, TRUE, ":", 0, &len);
         hex_str   = nm_utils_bin2hexstr_full(check_val ?: (guint8 *) str_value,
                                            check_val ? len : strlen(str_value),
diff --git a/src/core/dhcp/nm-dhcp-client.h b/src/core/dhcp/nm-dhcp-client.h
index a7b6ae98..12f3a276 100644
--- a/src/core/dhcp/nm-dhcp-client.h
+++ b/src/core/dhcp/nm-dhcp-client.h
@@ -53,8 +53,8 @@ typedef enum _nm_packed {
      * As such, it's never officially in a non-recoverable state.
      * However, there are cases when it really looks like we won't
      * be able to get a lease. For example, if the underlying interface
-     * is layer 3 only, if we have no IPv6 link local address for a prolonged
-     * time, or if dhclient is not installed.
+     * is layer 3 only, or if we have no IPv6 link local address for a prolonged
+     * time.
      * But even these cases are potentially recoverable. This is only
      * a hint to the user (which they might ignore).
      *
@@ -311,7 +311,6 @@ typedef struct {
 
 GType nm_dhcp_nettools_get_type(void);
 
-extern const NMDhcpClientFactory _nm_dhcp_client_factory_dhclient;
 extern const NMDhcpClientFactory _nm_dhcp_client_factory_dhcpcd;
 extern const NMDhcpClientFactory _nm_dhcp_client_factory_internal;
 extern const NMDhcpClientFactory _nm_dhcp_client_factory_systemd;
diff --git a/src/core/dhcp/nm-dhcp-dhclient-utils.c b/src/core/dhcp/nm-dhcp-dhclient-utils.c
deleted file mode 100644
index 286f7aa1..00000000
--- a/src/core/dhcp/nm-dhcp-dhclient-utils.c
+++ /dev/null
@@ -1,763 +0,0 @@
-/* SPDX-License-Identifier: GPL-2.0-or-later */
-/*
- * Copyright (C) 2011 Red Hat, Inc.
- */
-
-#include "src/core/nm-default-daemon.h"
-
-#include "nm-dhcp-dhclient-utils.h"
-
-#include <ctype.h>
-#include <arpa/inet.h>
-#include <net/if.h>
-#include <linux/if_ether.h>
-
-#include "libnm-glib-aux/nm-dedup-multi.h"
-
-#include "nm-dhcp-utils.h"
-#include "nm-utils.h"
-#include "libnm-platform/nm-platform.h"
-#include "NetworkManagerUtils.h"
-
-#define TIMEOUT_TAG  "timeout "
-#define RETRY_TAG    "retry "
-#define CLIENTID_TAG "send dhcp-client-identifier"
-
-#define HOSTNAME4_TAG    "send host-name"
-#define HOSTNAME4_FORMAT HOSTNAME4_TAG " \"%s\"; # added by NetworkManager"
-
-#define FQDN_TAG_PREFIX "send fqdn."
-#define FQDN_TAG        FQDN_TAG_PREFIX "fqdn"
-#define FQDN_FORMAT     FQDN_TAG " \"%s\"; # added by NetworkManager"
-
-#define ALSOREQ_TAG "also request "
-#define REQ_TAG     "request "
-
-#define MUDURLv4_DEF "option mudurl code 161 = text;\n"
-#define MUDURLv4_FMT "send mudurl \"%s\";\n"
-
-#define MUDURLv6_DEF "option dhcp6.mudurl code 112 = text;\n"
-#define MUDURLv6_FMT "send dhcp6.mudurl \"%s\";\n"
-
-static void
-add_request(GPtrArray *array, const char *item)
-{
-    guint i;
-
-    for (i = 0; i < array->len; i++) {
-        if (nm_streq(array->pdata[i], item))
-            return;
-    }
-    g_ptr_array_add(array, g_strdup(item));
-}
-
-static gboolean
-grab_request_options(GPtrArray *store, const char *line)
-{
-    gs_free const char **line_v = NULL;
-    gsize                i;
-
-    /* Grab each 'request' or 'also request'  option and save for later */
-    line_v = nm_strsplit_set(line, "\t ,");
-    for (i = 0; line_v && line_v[i]; i++) {
-        const char *ss = nm_str_skip_leading_spaces(line_v[i]);
-        gsize       l;
-        gboolean    end = FALSE;
-
-        if (!ss[0])
-            continue;
-        if (ss[0] == ';') {
-            /* all done */
-            return TRUE;
-        }
-
-        if (!g_ascii_isalnum(ss[0]))
-            continue;
-
-        l = strlen(ss);
-
-        while (l > 0 && g_ascii_isspace(ss[l - 1])) {
-            ((char *) ss)[l - 1] = '\0';
-            l--;
-        }
-        if (l > 0 && ss[l - 1] == ';') {
-            /* Remove the EOL marker */
-            ((char *) ss)[l - 1] = '\0';
-            end                  = TRUE;
-        }
-
-        if (ss[0])
-            add_request(store, ss);
-
-        if (end)
-            return TRUE;
-    }
-
-    return FALSE;
-}
-
-static void
-add_ip4_config(GString            *str,
-               GBytes             *client_id,
-               const char         *hostname,
-               gboolean            use_fqdn,
-               NMDhcpHostnameFlags hostname_flags)
-{
-    if (client_id) {
-        const char *p;
-        gsize       l;
-        guint       i;
-
-        p = g_bytes_get_data(client_id, &l);
-        nm_assert(p);
-
-        /* Allow type 0 (non-hardware address) to be represented as a string
-         * as long as all the characters are printable.
-         */
-        for (i = 1; (p[0] == 0) && i < l; i++) {
-            if (!g_ascii_isprint(p[i]) || p[i] == '\\' || p[i] == '"')
-                break;
-        }
-
-        g_string_append(str, CLIENTID_TAG " ");
-        if (l == 0) {
-            /* An empty value effectively unsets the client-id to avoid sending it */
-            g_string_append(str, "\"\"");
-        } else if (i < l) {
-            /* Unprintable; convert to a hex string */
-            for (i = 0; i < l; i++) {
-                if (i > 0)
-                    g_string_append_c(str, ':');
-                g_string_append_printf(str, "%02x", (guint8) p[i]);
-            }
-        } else {
-            /* Printable; just add to the line with type 0 */
-            g_string_append_c(str, '"');
-            g_string_append(str, "\\x00");
-            g_string_append_len(str, p + 1, l - 1);
-            g_string_append_c(str, '"');
-        }
-        g_string_append(str, "; # added by NetworkManager\n");
-    }
-
-    if (hostname) {
-        if (use_fqdn) {
-            g_string_append_printf(str, FQDN_FORMAT "\n", hostname);
-
-            g_string_append_printf(str,
-                                   FQDN_TAG_PREFIX "encoded %s;\n",
-                                   (hostname_flags & NM_DHCP_HOSTNAME_FLAG_FQDN_ENCODED) ? "on"
-                                                                                         : "off");
-
-            g_string_append_printf(
-                str,
-                FQDN_TAG_PREFIX "server-update %s;\n",
-                (hostname_flags & NM_DHCP_HOSTNAME_FLAG_FQDN_SERV_UPDATE) ? "on" : "off");
-
-            g_string_append_printf(str,
-                                   FQDN_TAG_PREFIX "no-client-update %s;\n",
-                                   (hostname_flags & NM_DHCP_HOSTNAME_FLAG_FQDN_NO_UPDATE) ? "on"
-                                                                                           : "off");
-        } else
-            g_string_append_printf(str, HOSTNAME4_FORMAT "\n", hostname);
-    }
-
-    g_string_append_c(str, '\n');
-
-    /* Define options for classless static routes */
-    g_string_append(
-        str,
-        "option rfc3442-classless-static-routes code 121 = array of unsigned integer 8;\n");
-    g_string_append(str,
-                    "option ms-classless-static-routes code 249 = array of unsigned integer 8;\n");
-    /* Web Proxy Auto-Discovery option (bgo #368423) */
-    g_string_append(str, "option wpad code 252 = string;\n");
-
-    g_string_append_c(str, '\n');
-}
-
-static void
-add_hostname6(GString *str, const char *hostname, NMDhcpHostnameFlags hostname_flags)
-{
-    if (hostname) {
-        g_string_append_printf(str, FQDN_FORMAT "\n", hostname);
-        if (hostname_flags & NM_DHCP_HOSTNAME_FLAG_FQDN_SERV_UPDATE)
-            g_string_append(str, FQDN_TAG_PREFIX "server-update on;\n");
-        if (hostname_flags & NM_DHCP_HOSTNAME_FLAG_FQDN_NO_UPDATE)
-            g_string_append(str, FQDN_TAG_PREFIX "no-client-update on;\n");
-        g_string_append_c(str, '\n');
-    }
-}
-
-static void
-add_mud_url_config(GString *str, const char *mud_url, int addr_family)
-{
-    if (mud_url) {
-        if (addr_family == AF_INET) {
-            g_string_append(str, MUDURLv4_DEF);
-            g_string_append_printf(str, MUDURLv4_FMT, mud_url);
-        } else {
-            g_string_append(str, MUDURLv6_DEF);
-            g_string_append_printf(str, MUDURLv6_FMT, mud_url);
-        }
-    }
-}
-
-static GBytes *
-read_client_id(const char *str)
-{
-    gs_free char *s = NULL;
-    char         *p;
-    int           i = 0;
-    int           j = 0;
-    gsize         l;
-
-    nm_assert(NM_STR_HAS_PREFIX(str, CLIENTID_TAG));
-    str += NM_STRLEN(CLIENTID_TAG);
-
-    if (!g_ascii_isspace(*str))
-        return NULL;
-    while (g_ascii_isspace(*str))
-        str++;
-
-    if (*str == '"') {
-        /* Parse string literal with escape sequences */
-        s = g_strdup(str + 1);
-        p = strrchr(s, '"');
-        if (p)
-            *p = '\0';
-        else
-            return NULL;
-
-        if (!s[0])
-            return NULL;
-
-        while (s[i]) {
-            if (s[i] == '\\' && s[i + 1] == 'x' && g_ascii_isxdigit(s[i + 2])
-                && g_ascii_isxdigit(s[i + 3])) {
-                s[j++] = (g_ascii_xdigit_value(s[i + 2]) << 4) + g_ascii_xdigit_value(s[i + 3]);
-                i += 4;
-                continue;
-            }
-            if (s[i] == '\\' && s[i + 1] >= '0' && s[i + 1] <= '7' && s[1 + 2] >= '0'
-                && s[i + 2] <= '7' && s[1 + 3] >= '0' && s[i + 3] <= '7') {
-                s[j++] = ((s[i + 1] - '0') << 6) + ((s[i + 2] - '0') << 3) + (s[i + 3] - '0');
-                i += 4;
-                continue;
-            }
-            s[j++] = s[i++];
-        }
-        return g_bytes_new_take(g_steal_pointer(&s), j);
-    }
-
-    /* Otherwise, try to read a hexadecimal sequence */
-    s = g_strdup(str);
-    g_strchomp(s);
-    l = strlen(s);
-    if (l > 0 && s[l - 1] == ';')
-        s[l - 1] = '\0';
-
-    return nm_utils_hexstr2bin(s);
-}
-
-static gboolean
-read_interface(const char *line, char *interface, guint size)
-{
-    gs_free char *dup = g_strdup(line + NM_STRLEN("interface"));
-    char         *ptr = dup, *end;
-
-    while (g_ascii_isspace(*ptr))
-        ptr++;
-
-    if (*ptr == '"') {
-        ptr++;
-        end = strchr(ptr, '"');
-        if (!end)
-            return FALSE;
-        *end = '\0';
-    } else {
-        end = strchr(ptr, ' ');
-        if (!end)
-            end = strchr(ptr, '{');
-        if (!end)
-            return FALSE;
-        *end = '\0';
-    }
-
-    if (ptr[0] == '\0' || strlen(ptr) + 1 > size)
-        return FALSE;
-
-    g_snprintf(interface, size, "%s", ptr);
-
-    return TRUE;
-}
-
-char *
-nm_dhcp_dhclient_create_config(const char         *interface,
-                               int                 addr_family,
-                               GBytes             *client_id,
-                               gboolean            send_client_id,
-                               const char         *anycast_address,
-                               const char         *hostname,
-                               guint32             timeout,
-                               gboolean            use_fqdn,
-                               NMDhcpHostnameFlags hostname_flags,
-                               const char         *mud_url,
-                               const char *const  *reject_servers,
-                               const char         *orig_path,
-                               const char         *orig_contents,
-                               GBytes            **out_new_client_id)
-{
-    nm_auto_free_gstring GString *new_contents  = NULL;
-    gs_unref_ptrarray GPtrArray  *fqdn_opts     = NULL;
-    gs_unref_ptrarray GPtrArray  *reqs          = NULL;
-    gboolean                      reset_reqlist = FALSE;
-    int                           i;
-
-    g_return_val_if_fail(!anycast_address || nm_utils_hwaddr_valid(anycast_address, ETH_ALEN),
-                         NULL);
-    g_return_val_if_fail(NM_IN_SET(addr_family, AF_INET, AF_INET6), NULL);
-    g_return_val_if_fail(!reject_servers || addr_family == AF_INET, NULL);
-    nm_assert(!out_new_client_id || !*out_new_client_id);
-
-    new_contents = g_string_new(_("# Created by NetworkManager\n"));
-    reqs         = g_ptr_array_new_full(5, g_free);
-
-    if (orig_contents) {
-        gs_free const char          **lines = NULL;
-        gsize                         line_i;
-        nm_auto_free_gstring GString *blocks_stack = NULL;
-        guint                         blocks_skip  = 0;
-        gboolean                      in_alsoreq   = FALSE;
-        gboolean                      in_req       = FALSE;
-        char                          intf[IFNAMSIZ];
-
-        blocks_stack = g_string_new(NULL);
-        g_string_append_printf(new_contents, _("# Merged from %s\n\n"), orig_path);
-        intf[0] = '\0';
-
-        lines = nm_strsplit_set(orig_contents, "\n\r");
-        for (line_i = 0; lines && lines[line_i]; line_i++) {
-            const char *line = nm_str_skip_leading_spaces(lines[line_i]);
-            const char *p;
-
-            if (line[0] == '\0')
-                continue;
-
-            g_strchomp((char *) line);
-
-            p = line;
-            if (in_req) {
-                /* pass */
-            } else if (strchr(p, '{')) {
-                if (NM_STR_HAS_PREFIX(p, "lease") || NM_STR_HAS_PREFIX(p, "alias")
-                    || NM_STR_HAS_PREFIX(p, "interface") || NM_STR_HAS_PREFIX(p, "pseudo")) {
-                    /* skip over these blocks, except 'interface' when it
-                     * matches the current interface */
-                    blocks_skip++;
-                    g_string_append_c(blocks_stack, 'b');
-                    if (!intf[0] && NM_STR_HAS_PREFIX(p, "interface")) {
-                        if (read_interface(p, intf, sizeof(intf)))
-                            continue;
-                    }
-                } else {
-                    /* allow other blocks (conditionals) */
-                    if (!strchr(p, '}')) /* '} else {'  */
-                        g_string_append_c(blocks_stack, 'c');
-                }
-            } else if (strchr(p, '}')) {
-                if (blocks_stack->len > 0) {
-                    if (blocks_stack->str[blocks_stack->len - 1] == 'b') {
-                        g_string_truncate(blocks_stack, blocks_stack->len - 1);
-                        nm_assert(blocks_skip > 0);
-                        blocks_skip--;
-                        intf[0] = '\0';
-                        continue;
-                    }
-                    g_string_truncate(blocks_stack, blocks_stack->len - 1);
-                }
-            }
-
-            if (blocks_skip > 0 && !intf[0])
-                continue;
-
-            if (intf[0] && !nm_streq(intf, interface))
-                continue;
-
-            /* Some timing parameters in dhclient should not be imported (timeout, retry).
-             * The retry parameter will be simply not used as we will exit on first failure.
-             * The timeout one instead may affect NetworkManager behavior: if the timeout
-             * elapses before dhcp-timeout dhclient will report failure and cause NM to
-             * fail the dhcp process before dhcp-timeout. So, always skip importing timeout
-             * as we will need to add one greater than dhcp-timeout.
-             */
-            if (NM_STR_HAS_PREFIX(p, TIMEOUT_TAG) || NM_STR_HAS_PREFIX(p, RETRY_TAG))
-                continue;
-
-            if (NM_STR_HAS_PREFIX(p, CLIENTID_TAG)) {
-                /* Skip "dhcp-client-id" if the connection has defined a custom one or "none" */
-                if (client_id || !send_client_id)
-                    continue;
-
-                /* Otherwise, capture and return the existing client id */
-                if (out_new_client_id)
-                    nm_clear_pointer(out_new_client_id, g_bytes_unref);
-                NM_SET_OUT(out_new_client_id, read_client_id(p));
-                /* fall-through. We keep the line... */
-            }
-
-            /* Override config file hostname and use one from the connection */
-            if (hostname) {
-                if (NM_STR_HAS_PREFIX(p, HOSTNAME4_TAG))
-                    continue;
-                if (NM_STR_HAS_PREFIX(p, FQDN_TAG))
-                    continue;
-            }
-
-            /* To let user's FQDN options (except "fqdn.fqdn") override the
-             * default ones set by NM, add them later
-             */
-            if (NM_STR_HAS_PREFIX(p, FQDN_TAG_PREFIX)) {
-                if (!fqdn_opts)
-                    fqdn_opts = g_ptr_array_new_full(5, g_free);
-                g_ptr_array_add(fqdn_opts, g_strdup(p + NM_STRLEN(FQDN_TAG_PREFIX)));
-                continue;
-            }
-
-            /* Ignore 'script' since we pass our own */
-            if (g_str_has_prefix(p, "script "))
-                continue;
-
-            /* Check for "request" */
-            if (NM_STR_HAS_PREFIX(p, REQ_TAG)) {
-                in_req = TRUE;
-                p += NM_STRLEN(REQ_TAG);
-                g_ptr_array_set_size(reqs, 0);
-                reset_reqlist = TRUE;
-            }
-
-            /* Save all request options for later use */
-            if (in_req) {
-                in_req = !grab_request_options(reqs, p);
-                continue;
-            }
-
-            /* Check for "also require" */
-            if (NM_STR_HAS_PREFIX(p, ALSOREQ_TAG)) {
-                in_alsoreq = TRUE;
-                p += NM_STRLEN(ALSOREQ_TAG);
-            }
-
-            if (in_alsoreq) {
-                in_alsoreq = !grab_request_options(reqs, p);
-                continue;
-            }
-
-            /* Existing configuration line is OK, add it to new configuration */
-            g_string_append(new_contents, line);
-            g_string_append_c(new_contents, '\n');
-        }
-    } else
-        g_string_append_c(new_contents, '\n');
-
-    /* ensure dhclient timeout is greater than dhcp-timeout: as dhclient timeout default value is
-     * 60 seconds, we need this only if dhcp-timeout is greater than 60.
-     */
-    if (timeout >= 60) {
-        timeout = timeout < G_MAXINT32 ? timeout + 1 : G_MAXINT32;
-        g_string_append_printf(new_contents, "timeout %u;\n", timeout);
-    }
-
-    add_mud_url_config(new_contents, mud_url, addr_family);
-
-    if (reject_servers && reject_servers[0]) {
-        g_string_append(new_contents, "reject ");
-        for (i = 0; reject_servers[i]; i++) {
-            if (i != 0)
-                g_string_append(new_contents, ", ");
-            g_string_append(new_contents, reject_servers[i]);
-        }
-        g_string_append(new_contents, ";\n");
-    }
-
-    if (addr_family == AF_INET) {
-        nm_auto_unref_bytes GBytes *client_id_none = NULL;
-        client_id = send_client_id ? client_id : (client_id_none = g_bytes_new_static("", 0));
-        add_ip4_config(new_contents, client_id, hostname, use_fqdn, hostname_flags);
-        add_request(reqs, "rfc3442-classless-static-routes");
-        add_request(reqs, "ms-classless-static-routes");
-        add_request(reqs, "static-routes");
-        add_request(reqs, "wpad");
-        add_request(reqs, "ntp-servers");
-        add_request(reqs, "root-path");
-    } else {
-        add_hostname6(new_contents, hostname, hostname_flags);
-        add_request(reqs, "dhcp6.name-servers");
-        add_request(reqs, "dhcp6.domain-search");
-
-        /* FIXME: internal client does not support requesting client-id option. Does this even work? */
-        add_request(reqs, "dhcp6.client-id");
-    }
-
-    if (reset_reqlist)
-        g_string_append(new_contents, "request; # override dhclient defaults\n");
-    /* And add it to the dhclient configuration */
-    for (i = 0; i < reqs->len; i++)
-        g_string_append_printf(new_contents, "also request %s;\n", (char *) reqs->pdata[i]);
-
-    if (fqdn_opts) {
-        for (i = 0; i < fqdn_opts->len; i++) {
-            const char *t = fqdn_opts->pdata[i];
-
-            if (i == 0)
-                g_string_append_printf(new_contents, "\n# FQDN options from %s\n", orig_path);
-            g_string_append_printf(new_contents, FQDN_TAG_PREFIX "%s\n", t);
-        }
-    }
-
-    g_string_append_c(new_contents, '\n');
-
-    if (anycast_address) {
-        g_string_append_printf(new_contents,
-                               "interface \"%s\" {\n"
-                               " initial-interval 1; \n"
-                               " anycast-mac ethernet %s;\n"
-                               "}\n",
-                               interface,
-                               anycast_address);
-    }
-
-    return g_string_free(g_steal_pointer(&new_contents), FALSE);
-}
-
-/* In the lease file, dhclient will write "option dhcp6.client-id $HEXSTR". This
- * function does the same. */
-static char *
-nm_dhcp_dhclient_escape_duid_as_hex(GBytes *duid)
-{
-    const guint8 *s;
-    gsize         len;
-
-    nm_assert(duid);
-
-    s = g_bytes_get_data(duid, &len);
-    return nm_utils_bin2hexstr_fuller(s, len, ':', FALSE, FALSE, NULL);
-}
-
-/* Roughly follow what dhclient's quotify_buf() and pretty_escape() functions do */
-char *
-nm_dhcp_dhclient_escape_duid(GBytes *duid)
-{
-    char         *escaped;
-    const guint8 *s, *s0;
-    gsize         len;
-    char         *d;
-
-    g_return_val_if_fail(duid, NULL);
-
-    s0 = g_bytes_get_data(duid, &len);
-    s  = s0;
-
-    d = escaped = g_malloc((len * 4) + 1);
-    while (s < (s0 + len)) {
-        if (!g_ascii_isprint(*s)) {
-            *d++ = '\\';
-            *d++ = '0' + ((*s >> 6) & 0x7);
-            *d++ = '0' + ((*s >> 3) & 0x7);
-            *d++ = '0' + (*s++ & 0x7);
-        } else if (*s == '"' || *s == '\'' || *s == '$' || *s == '`' || *s == '\\' || *s == '|'
-                   || *s == '&') {
-            *d++ = '\\';
-            *d++ = *s++;
-        } else
-            *d++ = *s++;
-    }
-    *d++ = '\0';
-    return escaped;
-}
-
-static gboolean
-isoctal(const guint8 *p)
-{
-    return (p[0] >= '0' && p[0] <= '3' && p[1] >= '0' && p[1] <= '7' && p[2] >= '0' && p[2] <= '7');
-}
-
-GBytes *
-nm_dhcp_dhclient_unescape_duid(const char *duid)
-{
-    GByteArray   *unescaped;
-    const guint8 *p = (const guint8 *) duid;
-    guint         i, len;
-    guint8        octal;
-
-    /* FIXME: it's wrong to have an "unescape-duid" function. dhclient
-     * defines a file format with escaping. So we need a general unescape
-     * function that can handle dhclient syntax. */
-
-    len       = strlen(duid);
-    unescaped = g_byte_array_sized_new(len);
-    for (i = 0; i < len; i++) {
-        if (p[i] == '\\') {
-            i++;
-            if (isdigit(p[i])) {
-                /* Octal escape sequence */
-                if (i + 2 >= len || !isoctal(p + i))
-                    goto error;
-                octal = ((p[i] - '0') << 6) + ((p[i + 1] - '0') << 3) + (p[i + 2] - '0');
-                g_byte_array_append(unescaped, &octal, 1);
-                i += 2;
-            } else {
-                /* FIXME: don't warn on untrusted data. Either signal an error, or accept
-                 * it silently. */
-
-                /* One of ", ', $, `, \, |, or & */
-                g_warn_if_fail(p[i] == '"' || p[i] == '\'' || p[i] == '$' || p[i] == '`'
-                               || p[i] == '\\' || p[i] == '|' || p[i] == '&');
-                g_byte_array_append(unescaped, &p[i], 1);
-            }
-        } else
-            g_byte_array_append(unescaped, &p[i], 1);
-    }
-
-    return g_byte_array_free_to_bytes(unescaped);
-
-error:
-    g_byte_array_free(unescaped, TRUE);
-    return NULL;
-}
-
-#define DEFAULT_DUID_PREFIX "default-duid \""
-
-/* Beware: @error may be unset even if the function returns %NULL. */
-GBytes *
-nm_dhcp_dhclient_read_duid(const char *leasefile, GError **error)
-{
-    gs_free char        *contents   = NULL;
-    gs_free const char **contents_v = NULL;
-    gsize                i;
-
-    if (!g_file_test(leasefile, G_FILE_TEST_EXISTS))
-        return NULL;
-
-    if (!g_file_get_contents(leasefile, &contents, NULL, error))
-        return NULL;
-
-    contents_v = nm_strsplit_set(contents, "\n\r");
-    for (i = 0; contents_v && contents_v[i]; i++) {
-        const char *p = nm_str_skip_leading_spaces(contents_v[i]);
-        GBytes     *duid;
-
-        if (!NM_STR_HAS_PREFIX(p, DEFAULT_DUID_PREFIX))
-            continue;
-
-        p += NM_STRLEN(DEFAULT_DUID_PREFIX);
-
-        g_strchomp((char *) p);
-
-        if (!NM_STR_HAS_SUFFIX(p, "\";"))
-            continue;
-
-        ((char *) p)[strlen(p) - 2] = '\0';
-
-        duid = nm_dhcp_dhclient_unescape_duid(p);
-        if (duid)
-            return duid;
-    }
-
-    return NULL;
-}
-
-gboolean
-nm_dhcp_dhclient_save_duid(const char *leasefile,
-                           GBytes     *duid,
-                           gboolean    enforce_duid,
-                           GError    **error)
-{
-    gs_free char                 *escaped_duid = NULL;
-    gs_free const char          **lines        = NULL;
-    nm_auto_free_gstring GString *s            = NULL;
-    const char *const            *iter;
-    gs_free char                 *conflicting_duid_line = NULL;
-    gs_free char                 *contents              = NULL;
-    gsize                         contents_len          = 0;
-
-    g_return_val_if_fail(leasefile != NULL, FALSE);
-
-    if (!duid) {
-        nm_utils_error_set_literal(error, NM_UTILS_ERROR_UNKNOWN, "missing duid");
-        g_return_val_if_reached(FALSE);
-    }
-
-    escaped_duid = nm_dhcp_dhclient_escape_duid(duid);
-    nm_assert(escaped_duid);
-
-    if (g_file_test(leasefile, G_FILE_TEST_EXISTS)) {
-        if (!g_file_get_contents(leasefile, &contents, &contents_len, error)) {
-            g_prefix_error(error, "failed to read lease file %s: ", leasefile);
-            return FALSE;
-        }
-
-        lines = nm_strsplit_set_with_empty(contents, "\n");
-    }
-
-    s = g_string_sized_new(contents_len + 50);
-    g_string_append_printf(s, DEFAULT_DUID_PREFIX "%s\";\n", escaped_duid);
-
-    /* Preserve existing leasefile contents */
-    if (lines) {
-        for (iter = lines; *iter; iter++) {
-            const char *str = *iter;
-            const char *l;
-            gboolean    ends_with_r;
-            gsize       l_len;
-            gsize       prefix_len;
-
-            l          = nm_str_skip_leading_spaces(str);
-            l_len      = strlen(l);
-            prefix_len = l - str;
-
-            ends_with_r = l_len > 0 && l[l_len - 1u] == '\r';
-            if (ends_with_r) {
-                ((char *) l)[--l_len] = '\0';
-            }
-
-            if (NM_STR_HAS_PREFIX(l, DEFAULT_DUID_PREFIX)) {
-                /* We always add our line on top. This line can be skipped. */
-                continue;
-            }
-
-            if (enforce_duid & NM_STR_HAS_PREFIX(l, "option dhcp6.client-id ")) {
-                /* we want to use our duid. Skip the per-lease client-id. */
-                if (!conflicting_duid_line) {
-                    gs_free char *duid_hex = nm_dhcp_dhclient_escape_duid_as_hex(duid);
-
-                    conflicting_duid_line = g_strdup_printf("option dhcp6.client-id %s;", duid_hex);
-                }
-                /* We adjust the duid line and set what we want. */
-                l = conflicting_duid_line;
-            }
-
-            g_string_append_len(s, str, prefix_len);
-            g_string_append(s, l);
-            if (ends_with_r) {
-                g_string_append_c(s, '\r');
-                g_string_append_c(s, '\n');
-            } else if ((iter[1]) != NULL) {
-                /* avoid to add an extra '\n' at the end of file */
-                g_string_append_c(s, '\n');
-            }
-        }
-    }
-
-    if (contents && strlen(contents) == contents_len && nm_streq(contents, s->str)) {
-        /* The file is already as we want it. We are done. */
-        return TRUE;
-    }
-
-    if (!g_file_set_contents(leasefile, s->str, -1, error)) {
-        g_prefix_error(error, "failed to set DUID in lease file %s: ", leasefile);
-        return FALSE;
-    }
-
-    return TRUE;
-}
diff --git a/src/core/dhcp/nm-dhcp-dhclient-utils.h b/src/core/dhcp/nm-dhcp-dhclient-utils.h
deleted file mode 100644
index 34b26175..00000000
--- a/src/core/dhcp/nm-dhcp-dhclient-utils.h
+++ /dev/null
@@ -1,38 +0,0 @@
-/* SPDX-License-Identifier: GPL-2.0-or-later */
-/*
- * Copyright (C) 2010 Red Hat, Inc.
- */
-
-#ifndef __NETWORKMANAGER_DHCP_DHCLIENT_UTILS_H__
-#define __NETWORKMANAGER_DHCP_DHCLIENT_UTILS_H__
-
-#include "nm-setting-ip4-config.h"
-#include "nm-setting-ip6-config.h"
-
-char *nm_dhcp_dhclient_create_config(const char         *interface,
-                                     int                 addr_family,
-                                     GBytes             *client_id,
-                                     gboolean            send_client_id,
-                                     const char         *anycast_addr,
-                                     const char         *hostname,
-                                     guint32             timeout,
-                                     gboolean            use_fqdn,
-                                     NMDhcpHostnameFlags hostname_flags,
-                                     const char         *mud_url,
-                                     const char *const  *reject_servers,
-                                     const char         *orig_path,
-                                     const char         *orig_contents,
-                                     GBytes            **out_new_client_id);
-
-char *nm_dhcp_dhclient_escape_duid(GBytes *duid);
-
-GBytes *nm_dhcp_dhclient_unescape_duid(const char *duid);
-
-GBytes *nm_dhcp_dhclient_read_duid(const char *leasefile, GError **error);
-
-gboolean nm_dhcp_dhclient_save_duid(const char *leasefile,
-                                    GBytes     *duid,
-                                    gboolean    enforce_duid,
-                                    GError    **error);
-
-#endif /* __NETWORKMANAGER_DHCP_DHCLIENT_UTILS_H__ */
diff --git a/src/core/dhcp/nm-dhcp-dhclient.c b/src/core/dhcp/nm-dhcp-dhclient.c
deleted file mode 100644
index 7e00599c..00000000
--- a/src/core/dhcp/nm-dhcp-dhclient.c
+++ /dev/null
@@ -1,741 +0,0 @@
-/* SPDX-License-Identifier: GPL-2.0-or-later */
-/*
- * Copyright (C) 2005 - 2012 Red Hat, Inc.
- */
-
-#include <config.h>
-#define __CONFIG_H__
-
-#define _XOPEN_SOURCE
-#include <time.h>
-#undef _XOPEN_SOURCE
-
-#include "src/core/nm-default-daemon.h"
-
-#if WITH_DHCLIENT
-
-#include <stdlib.h>
-#include <unistd.h>
-#include <stdio.h>
-#include <netinet/in.h>
-#include <arpa/inet.h>
-#include <ctype.h>
-
-#include "libnm-glib-aux/nm-dedup-multi.h"
-
-#include "nm-utils.h"
-#include "nm-dhcp-dhclient-utils.h"
-#include "nm-dhcp-manager.h"
-#include "NetworkManagerUtils.h"
-#include "nm-dhcp-listener.h"
-#include "nm-dhcp-client-logging.h"
-
-/*****************************************************************************/
-
-static const char *
-_addr_family_to_path_part(int addr_family)
-{
-    nm_assert(NM_IN_SET(addr_family, AF_INET, AF_INET6));
-    return (addr_family == AF_INET6) ? "6" : "";
-}
-
-/*****************************************************************************/
-
-#define NM_TYPE_DHCP_DHCLIENT (nm_dhcp_dhclient_get_type())
-#define NM_DHCP_DHCLIENT(obj) \
-    (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DHCP_DHCLIENT, NMDhcpDhclient))
-#define NM_DHCP_DHCLIENT_CLASS(klass) \
-    (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_DHCP_DHCLIENT, NMDhcpDhclientClass))
-#define NM_IS_DHCP_DHCLIENT(obj)         (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_DHCP_DHCLIENT))
-#define NM_IS_DHCP_DHCLIENT_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), NM_TYPE_DHCP_DHCLIENT))
-#define NM_DHCP_DHCLIENT_GET_CLASS(obj) \
-    (G_TYPE_INSTANCE_GET_CLASS((obj), NM_TYPE_DHCP_DHCLIENT, NMDhcpDhclientClass))
-
-typedef struct _NMDhcpDhclient      NMDhcpDhclient;
-typedef struct _NMDhcpDhclientClass NMDhcpDhclientClass;
-
-static GType nm_dhcp_dhclient_get_type(void);
-
-/*****************************************************************************/
-
-typedef struct {
-    char           *conf_file;
-    const char     *def_leasefile;
-    char           *lease_file;
-    char           *pid_file;
-    NMDhcpListener *dhcp_listener;
-} NMDhcpDhclientPrivate;
-
-struct _NMDhcpDhclient {
-    NMDhcpClient          parent;
-    NMDhcpDhclientPrivate _priv;
-};
-
-struct _NMDhcpDhclientClass {
-    NMDhcpClientClass parent;
-};
-
-G_DEFINE_TYPE(NMDhcpDhclient, nm_dhcp_dhclient, NM_TYPE_DHCP_CLIENT)
-
-#define NM_DHCP_DHCLIENT_GET_PRIVATE(self) \
-    _NM_GET_PRIVATE(self, NMDhcpDhclient, NM_IS_DHCP_DHCLIENT)
-
-/*****************************************************************************/
-
-static GBytes *read_duid_from_lease(NMDhcpDhclient *self);
-
-/*****************************************************************************/
-
-static const char *
-nm_dhcp_dhclient_get_path(void)
-{
-    return nm_utils_find_helper("dhclient", DHCLIENT_PATH, NULL);
-}
-
-/**
- * get_dhclient_leasefile():
- * @addr_family: AF_INET or AF_INET6
- * @iface: the interface name of the device on which DHCP will be done
- * @uuid: the connection UUID to which the returned lease should belong
- * @out_preferred_path: on return, the "most preferred" leasefile path
- *
- * Returns the path of an existing leasefile (if any) for this interface and
- * connection UUID.  Also returns the "most preferred" leasefile path, which
- * may be different than any found leasefile.
- *
- * Returns: an existing leasefile, or %NULL if no matching leasefile could be found
- */
-static char *
-get_dhclient_leasefile(int         addr_family,
-                       const char *iface,
-                       const char *uuid,
-                       char      **out_preferred_path)
-{
-    gs_free char *path = NULL;
-
-    if (nm_dhcp_utils_get_leasefile_path(addr_family, "dhclient", iface, uuid, &path)) {
-        NM_SET_OUT(out_preferred_path, g_strdup(path));
-        return g_steal_pointer(&path);
-    }
-
-    NM_SET_OUT(out_preferred_path, g_steal_pointer(&path));
-
-    /* If the leasefile we're looking for doesn't exist yet in the new location
-     * (eg, /var/lib/NetworkManager) then look in old locations to maintain
-     * backwards compatibility with external tools (like dracut) that put
-     * leasefiles there.
-     */
-
-    /* Old Debian, SUSE, and Mandriva location */
-    g_free(path);
-    path = g_strdup_printf(LOCALSTATEDIR "/lib/dhcp/dhclient%s-%s-%s.lease",
-                           _addr_family_to_path_part(addr_family),
-                           uuid,
-                           iface);
-    if (g_file_test(path, G_FILE_TEST_EXISTS))
-        return g_steal_pointer(&path);
-
-    /* Old Red Hat and Fedora location */
-    g_free(path);
-    path = g_strdup_printf(LOCALSTATEDIR "/lib/dhclient/dhclient%s-%s-%s.lease",
-                           _addr_family_to_path_part(addr_family),
-                           uuid,
-                           iface);
-    if (g_file_test(path, G_FILE_TEST_EXISTS))
-        return g_steal_pointer(&path);
-
-    /* Fail */
-    return NULL;
-}
-
-static char *
-find_existing_config(NMDhcpDhclient *self, int addr_family, const char *iface, const char *uuid)
-{
-    char *path;
-
-    /* NetworkManager-overridden configuration can be used to ship DHCP config
-     * with NetworkManager itself. It can be uuid-specific, device-specific
-     * or generic.
-     */
-    if (uuid) {
-        path = g_strdup_printf(NMCONFDIR "/dhclient%s-%s.conf",
-                               _addr_family_to_path_part(addr_family),
-                               uuid);
-        _LOGD("looking for existing config %s", path);
-        if (g_file_test(path, G_FILE_TEST_EXISTS))
-            return path;
-        g_free(path);
-    }
-
-    path = g_strdup_printf(NMCONFDIR "/dhclient%s-%s.conf",
-                           _addr_family_to_path_part(addr_family),
-                           iface);
-    _LOGD("looking for existing config %s", path);
-    if (g_file_test(path, G_FILE_TEST_EXISTS))
-        return path;
-    g_free(path);
-
-    path = g_strdup_printf(NMCONFDIR "/dhclient%s.conf", _addr_family_to_path_part(addr_family));
-    _LOGD("looking for existing config %s", path);
-    if (g_file_test(path, G_FILE_TEST_EXISTS))
-        return path;
-    g_free(path);
-
-    /* Distribution's dhclient configuration is used so that we can use
-     * configuration shipped with dhclient (if any).
-     *
-     * This replaces conditional compilation based on distribution name. Fedora
-     * and Debian store the configs in /etc/dhcp while upstream defaults to /etc
-     * which is then used by many other distributions. Some distributions
-     * (including Fedora) don't even provide a default configuration file.
-     */
-    path = g_strdup_printf(SYSCONFDIR "/dhcp/dhclient%s-%s.conf",
-                           _addr_family_to_path_part(addr_family),
-                           iface);
-    _LOGD("looking for existing config %s", path);
-    if (g_file_test(path, G_FILE_TEST_EXISTS))
-        return path;
-    g_free(path);
-
-    path = g_strdup_printf(SYSCONFDIR "/dhclient%s-%s.conf",
-                           _addr_family_to_path_part(addr_family),
-                           iface);
-    _LOGD("looking for existing config %s", path);
-    if (g_file_test(path, G_FILE_TEST_EXISTS))
-        return path;
-    g_free(path);
-
-    path =
-        g_strdup_printf(SYSCONFDIR "/dhcp/dhclient%s.conf", _addr_family_to_path_part(addr_family));
-    _LOGD("looking for existing config %s", path);
-    if (g_file_test(path, G_FILE_TEST_EXISTS))
-        return path;
-    g_free(path);
-
-    path = g_strdup_printf(SYSCONFDIR "/dhclient%s.conf", _addr_family_to_path_part(addr_family));
-    _LOGD("looking for existing config %s", path);
-    if (g_file_test(path, G_FILE_TEST_EXISTS))
-        return path;
-    g_free(path);
-
-    return NULL;
-}
-
-/* NM provides interface-specific options; thus the same dhclient config
- * file cannot be used since DHCP transactions can happen in parallel.
- * Since some distros don't have default per-interface dhclient config files,
- * read their single config file and merge that into a custom per-interface
- * config file along with the NM options.
- */
-static char *
-create_dhclient_config(NMDhcpDhclient     *self,
-                       int                 addr_family,
-                       const char         *iface,
-                       const char         *uuid,
-                       GBytes             *client_id,
-                       gboolean            send_client_id,
-                       const char         *anycast_address,
-                       const char         *hostname,
-                       guint32             timeout,
-                       gboolean            use_fqdn,
-                       NMDhcpHostnameFlags hostname_flags,
-                       const char         *mud_url,
-                       const char *const  *reject_servers,
-                       GBytes            **out_new_client_id)
-{
-    gs_free char *orig_path    = NULL;
-    gs_free char *orig_content = NULL;
-    char         *new_path     = NULL;
-    gs_free char *new_content  = NULL;
-    GError       *error        = NULL;
-
-    g_return_val_if_fail(iface != NULL, NULL);
-
-    new_path = g_strdup_printf(NMSTATEDIR "/dhclient%s-%s.conf",
-                               _addr_family_to_path_part(addr_family),
-                               iface);
-    _LOGD("creating composite dhclient config %s", new_path);
-
-    orig_path = find_existing_config(self, addr_family, iface, uuid);
-    if (orig_path)
-        _LOGD("merging existing dhclient config %s", orig_path);
-    else
-        _LOGD("no existing dhclient configuration to merge");
-
-    if (orig_path && g_file_test(orig_path, G_FILE_TEST_EXISTS)) {
-        if (!g_file_get_contents(orig_path, &orig_content, NULL, &error)) {
-            _LOGW("error reading dhclient configuration %s: %s", orig_path, error->message);
-            g_error_free(error);
-        }
-    }
-
-    new_content = nm_dhcp_dhclient_create_config(iface,
-                                                 addr_family,
-                                                 client_id,
-                                                 send_client_id,
-                                                 anycast_address,
-                                                 hostname,
-                                                 timeout,
-                                                 use_fqdn,
-                                                 hostname_flags,
-                                                 mud_url,
-                                                 reject_servers,
-                                                 orig_path,
-                                                 orig_content,
-                                                 out_new_client_id);
-    nm_assert(new_content);
-
-    if (!g_file_set_contents(new_path, new_content, -1, &error)) {
-        _LOGW("error creating dhclient configuration: %s", error->message);
-        g_error_free(error);
-        g_free(new_path);
-        return NULL;
-    }
-
-    return new_path;
-}
-
-static gboolean
-dhclient_start(NMDhcpClient *client,
-               gboolean      set_mode,
-               gboolean      release,
-               gboolean      set_duid,
-               pid_t        *out_pid,
-               GError      **error)
-{
-    NMDhcpDhclient              *self = NM_DHCP_DHCLIENT(client);
-    NMDhcpDhclientPrivate       *priv = NM_DHCP_DHCLIENT_GET_PRIVATE(self);
-    gs_unref_ptrarray GPtrArray *argv = NULL;
-    pid_t                        pid;
-    gs_free_error GError        *local = NULL;
-    const char                  *iface;
-    const char                  *uuid;
-    const char                  *system_bus_address;
-    const char                  *dhclient_path;
-    char                        *binary_name;
-    gs_free char                *cmd_str                  = NULL;
-    gs_free char                *pid_file                 = NULL;
-    gs_free char                *system_bus_address_env   = NULL;
-    gs_free char                *preferred_leasefile_path = NULL;
-    int                          addr_family;
-    const NMDhcpClientConfig    *client_config;
-    char                         pd_length_str[16];
-
-    g_return_val_if_fail(!priv->pid_file, FALSE);
-    client_config = nm_dhcp_client_get_config(client);
-    addr_family   = client_config->addr_family;
-
-    NM_SET_OUT(out_pid, 0);
-
-    dhclient_path = nm_dhcp_dhclient_get_path();
-    if (!dhclient_path) {
-        nm_utils_error_set_literal(error, NM_UTILS_ERROR_UNKNOWN, "dhclient binary not found");
-        return FALSE;
-    }
-
-    iface = client_config->iface;
-    uuid  = client_config->uuid;
-
-    pid_file = g_strdup_printf(NMRUNDIR "/dhclient%s-%s.pid",
-                               _addr_family_to_path_part(addr_family),
-                               iface);
-
-    /* Kill any existing dhclient from the pidfile */
-    binary_name = g_path_get_basename(dhclient_path);
-    nm_dhcp_client_stop_existing(pid_file, binary_name);
-    g_free(binary_name);
-
-    if (release) {
-        /* release doesn't use the pidfile after killing an old client */
-        nm_clear_g_free(&pid_file);
-    }
-
-    g_free(priv->lease_file);
-    priv->lease_file = get_dhclient_leasefile(addr_family, iface, uuid, &preferred_leasefile_path);
-    nm_assert(preferred_leasefile_path);
-    if (!priv->lease_file) {
-        /* No existing leasefile, dhclient will create one at the preferred path */
-        priv->lease_file = g_steal_pointer(&preferred_leasefile_path);
-    } else if (!nm_streq0(priv->lease_file, preferred_leasefile_path)) {
-        gs_unref_object GFile *src = g_file_new_for_path(priv->lease_file);
-        gs_unref_object GFile *dst = g_file_new_for_path(preferred_leasefile_path);
-
-        /* Try to copy the existing leasefile to the preferred location */
-        if (!g_file_copy(src, dst, G_FILE_COPY_OVERWRITE, NULL, NULL, NULL, &local)) {
-            gs_free char *s_path = NULL;
-            gs_free char *d_path = NULL;
-
-            /* Failure; just use the existing leasefile */
-            _LOGW("failed to copy leasefile %s to %s: %s",
-                  (s_path = g_file_get_path(src)),
-                  (d_path = g_file_get_path(dst)),
-                  local->message);
-            g_clear_error(&local);
-        } else {
-            /* Success; use the preferred leasefile path */
-            g_free(priv->lease_file);
-            priv->lease_file = g_file_get_path(dst);
-        }
-    }
-
-    /* Save the DUID to the leasefile dhclient will actually use */
-    if (set_duid && addr_family == AF_INET6) {
-        if (!nm_dhcp_dhclient_save_duid(priv->lease_file,
-                                        nm_dhcp_client_get_effective_client_id(client),
-                                        client_config->v6.enforce_duid,
-                                        &local)) {
-            nm_utils_error_set(error,
-                               NM_UTILS_ERROR_UNKNOWN,
-                               "failed to save DUID to '%s': %s",
-                               priv->lease_file,
-                               local->message);
-            return FALSE;
-        }
-    }
-
-    argv = g_ptr_array_new();
-    g_ptr_array_add(argv, (gpointer) dhclient_path);
-
-    g_ptr_array_add(argv, (gpointer) "-d");
-
-    /* Be quiet. dhclient logs to syslog anyway. And we duplicate the syslog
-     * to stderr in case of NM running with --debug.
-     */
-    g_ptr_array_add(argv, (gpointer) "-q");
-
-    if (release)
-        g_ptr_array_add(argv, (gpointer) "-r");
-
-    if (!release && client_config->addr_family == AF_INET && client_config->v4.request_broadcast) {
-        g_ptr_array_add(argv, (gpointer) "-B");
-    }
-
-    if (addr_family == AF_INET6) {
-        guint       prefixes = client_config->v6.needed_prefixes;
-        const char *mode_opt;
-
-        g_ptr_array_add(argv, (gpointer) "-6");
-
-        if (!set_mode)
-            mode_opt = NULL;
-        else if (!client_config->v6.info_only)
-            mode_opt = "-N";
-        else if (prefixes == 0)
-            mode_opt = "-S";
-        else
-            mode_opt = NULL;
-
-        if (mode_opt)
-            g_ptr_array_add(argv, (gpointer) mode_opt);
-
-        if (prefixes > 0 && client_config->v6.pd_hint_length > 0) {
-            if (!IN6_IS_ADDR_UNSPECIFIED(&client_config->v6.pd_hint_addr)) {
-                _LOGW("dhclient only supports a length as prefix delegation hint, not a prefix");
-            }
-
-            nm_sprintf_buf(pd_length_str, "%u", client_config->v6.pd_hint_length);
-            g_ptr_array_add(argv, "--prefix-len-hint");
-            g_ptr_array_add(argv, pd_length_str);
-        }
-
-        while (prefixes--)
-            g_ptr_array_add(argv, (gpointer) "-P");
-    }
-    g_ptr_array_add(argv, (gpointer) "-sf"); /* Set script file */
-    g_ptr_array_add(argv, (gpointer) nm_dhcp_helper_path);
-
-    if (pid_file) {
-        g_ptr_array_add(argv, (gpointer) "-pf"); /* Set pid file */
-        g_ptr_array_add(argv, (gpointer) pid_file);
-    }
-
-    g_ptr_array_add(argv, (gpointer) "-lf"); /* Set lease file */
-    g_ptr_array_add(argv, (gpointer) priv->lease_file);
-
-    if (priv->conf_file) {
-        g_ptr_array_add(argv, (gpointer) "-cf"); /* Set interface config file */
-        g_ptr_array_add(argv, (gpointer) priv->conf_file);
-    }
-
-    if (client_config->v4.dscp_explicit) {
-        _LOGW("dhclient does not support specifying a custom DSCP value; the TOS field will be set "
-              "to LOWDELAY (0x10).");
-    }
-
-    if (client_config->v4.ipv6_only_preferred) {
-        _LOGW("the dhclient backend does not support the \"IPv6-Only Preferred\" option; ignoring "
-              "it");
-    }
-
-    /* Usually the system bus address is well-known; but if it's supposed
-     * to be something else, we need to push it to dhclient, since dhclient
-     * sanitizes the environment it gives the action scripts.
-     */
-    system_bus_address = getenv("DBUS_SYSTEM_BUS_ADDRESS");
-    if (system_bus_address) {
-        system_bus_address_env = g_strdup_printf("DBUS_SYSTEM_BUS_ADDRESS=%s", system_bus_address);
-        g_ptr_array_add(argv, (gpointer) "-e");
-        g_ptr_array_add(argv, (gpointer) system_bus_address_env);
-    }
-
-    g_ptr_array_add(argv, (gpointer) iface);
-    g_ptr_array_add(argv, NULL);
-
-    _LOGD("running: %s", (cmd_str = g_strjoinv(" ", (char **) argv->pdata)));
-
-    if (!g_spawn_async(NULL,
-                       (char **) argv->pdata,
-                       NULL,
-                       G_SPAWN_DO_NOT_REAP_CHILD | G_SPAWN_STDOUT_TO_DEV_NULL
-                           | G_SPAWN_STDERR_TO_DEV_NULL,
-                       nm_utils_setpgid,
-                       NULL,
-                       &pid,
-                       &local)) {
-        nm_utils_error_set(error,
-                           NM_UTILS_ERROR_UNKNOWN,
-                           "dhclient failed to start: %s",
-                           local->message);
-        return FALSE;
-    }
-
-    _LOGI("dhclient started with pid %lld", (long long int) pid);
-
-    if (!release)
-        nm_dhcp_client_watch_child(client, pid);
-
-    priv->pid_file = g_steal_pointer(&pid_file);
-
-    NM_SET_OUT(out_pid, pid);
-    return TRUE;
-}
-
-static gboolean
-ip4_start(NMDhcpClient *client, GError **error)
-{
-    NMDhcpDhclient           *self          = NM_DHCP_DHCLIENT(client);
-    NMDhcpDhclientPrivate    *priv          = NM_DHCP_DHCLIENT_GET_PRIVATE(self);
-    gs_unref_bytes GBytes    *new_client_id = NULL;
-    const NMDhcpClientConfig *client_config;
-
-    client_config = nm_dhcp_client_get_config(client);
-
-    nm_assert(client_config->addr_family == AF_INET);
-
-    priv->conf_file = create_dhclient_config(self,
-                                             AF_INET,
-                                             client_config->iface,
-                                             client_config->uuid,
-                                             client_config->client_id,
-                                             client_config->v4.send_client_id,
-                                             client_config->anycast_address,
-                                             client_config->hostname,
-                                             client_config->timeout,
-                                             client_config->use_fqdn,
-                                             client_config->hostname_flags,
-                                             client_config->mud_url,
-                                             client_config->reject_servers,
-                                             &new_client_id);
-    if (!priv->conf_file) {
-        nm_utils_error_set_literal(error,
-                                   NM_UTILS_ERROR_UNKNOWN,
-                                   "error creating dhclient configuration file");
-        return FALSE;
-    }
-
-    /* Note that the effective-client-id for IPv4 here might be unknown/NULL. */
-    nm_assert(!new_client_id || !client_config->client_id);
-    nm_dhcp_client_set_effective_client_id(client, client_config->client_id ?: new_client_id);
-
-    return dhclient_start(client, FALSE, FALSE, FALSE, NULL, error);
-}
-
-static gboolean
-ip6_start(NMDhcpClient *client, const struct in6_addr *ll_addr, GError **error)
-{
-    NMDhcpDhclient           *self = NM_DHCP_DHCLIENT(client);
-    NMDhcpDhclientPrivate    *priv = NM_DHCP_DHCLIENT_GET_PRIVATE(self);
-    const NMDhcpClientConfig *config;
-    gs_unref_bytes GBytes    *effective_client_id = NULL;
-
-    config = nm_dhcp_client_get_config(client);
-
-    nm_assert(config->addr_family == AF_INET6);
-
-    if (config->v6.iaid_explicit)
-        _LOGW("dhclient does not support specifying an IAID for DHCPv6, it will be ignored");
-
-    priv->conf_file = create_dhclient_config(self,
-                                             AF_INET6,
-                                             config->iface,
-                                             config->uuid,
-                                             NULL,
-                                             TRUE,
-                                             config->anycast_address,
-                                             config->hostname,
-                                             config->timeout,
-                                             TRUE,
-                                             config->hostname_flags,
-                                             config->mud_url,
-                                             NULL,
-                                             NULL);
-    if (!priv->conf_file) {
-        nm_utils_error_set_literal(error,
-                                   NM_UTILS_ERROR_UNKNOWN,
-                                   "error creating dhclient configuration file");
-        return FALSE;
-    }
-
-    nm_assert(config->client_id);
-    if (!config->v6.enforce_duid)
-        effective_client_id = read_duid_from_lease(self);
-    nm_dhcp_client_set_effective_client_id(client, effective_client_id ?: config->client_id);
-
-    return dhclient_start(client, TRUE, FALSE, TRUE, NULL, error);
-}
-
-static void
-stop(NMDhcpClient *client, gboolean release)
-{
-    NMDhcpDhclient        *self = NM_DHCP_DHCLIENT(client);
-    NMDhcpDhclientPrivate *priv = NM_DHCP_DHCLIENT_GET_PRIVATE(self);
-    int                    errsv;
-
-    NM_DHCP_CLIENT_CLASS(nm_dhcp_dhclient_parent_class)->stop(client, release);
-
-    if (priv->conf_file)
-        if (remove(priv->conf_file) == -1) {
-            errsv = errno;
-            _LOGD("could not remove dhcp config file \"%s\": %d (%s)",
-                  priv->conf_file,
-                  errsv,
-                  nm_strerror_native(errsv));
-        }
-    if (priv->pid_file) {
-        if (remove(priv->pid_file) == -1) {
-            errsv = errno;
-            _LOGD("could not remove dhcp pid file \"%s\": %s (%d)",
-                  priv->pid_file,
-                  nm_strerror_native(errsv),
-                  errsv);
-        }
-        nm_clear_g_free(&priv->pid_file);
-    }
-
-    if (release) {
-        pid_t rpid = -1;
-
-        if (dhclient_start(client, FALSE, TRUE, FALSE, &rpid, NULL)) {
-            /* Wait a few seconds for the release to happen */
-            nm_dhcp_client_stop_pid(rpid, nm_dhcp_client_get_iface(client));
-        }
-    }
-}
-
-static GBytes *
-read_duid_from_lease(NMDhcpDhclient *self)
-{
-    NMDhcpClient             *client = NM_DHCP_CLIENT(self);
-    NMDhcpDhclientPrivate    *priv   = NM_DHCP_DHCLIENT_GET_PRIVATE(self);
-    const NMDhcpClientConfig *client_config;
-    GBytes                   *duid      = NULL;
-    gs_free char             *leasefile = NULL;
-    GError                   *error     = NULL;
-
-    client_config = nm_dhcp_client_get_config(client);
-
-    /* Look in interface-specific leasefile first for backwards compat */
-    leasefile = get_dhclient_leasefile(AF_INET6,
-                                       nm_dhcp_client_get_iface(client),
-                                       client_config->uuid,
-                                       NULL);
-    if (leasefile) {
-        _LOGD("looking for DUID in '%s'", leasefile);
-        duid = nm_dhcp_dhclient_read_duid(leasefile, &error);
-        if (error) {
-            _LOGW("failed to read leasefile '%s': %s", leasefile, error->message);
-            g_clear_error(&error);
-        }
-        if (duid)
-            return duid;
-    }
-
-    /* Otherwise, read the default machine-wide DUID */
-    _LOGD("looking for default DUID in '%s'", priv->def_leasefile);
-    duid = nm_dhcp_dhclient_read_duid(priv->def_leasefile, &error);
-    if (error) {
-        _LOGW("failed to read leasefile '%s': %s", priv->def_leasefile, error->message);
-        g_clear_error(&error);
-    }
-
-    return duid;
-}
-
-/*****************************************************************************/
-
-static void
-nm_dhcp_dhclient_init(NMDhcpDhclient *self)
-{
-    static const char *const FILES[] = {
-        SYSCONFDIR "/dhclient6.leases", /* default */
-        LOCALSTATEDIR "/lib/dhcp/dhclient6.leases",
-        LOCALSTATEDIR "/lib/dhclient/dhclient6.leases",
-    };
-    NMDhcpDhclientPrivate *priv = NM_DHCP_DHCLIENT_GET_PRIVATE(self);
-    int                    i;
-
-    priv->def_leasefile = FILES[0];
-    for (i = 0; i < G_N_ELEMENTS(FILES); i++) {
-        if (g_file_test(FILES[i], G_FILE_TEST_EXISTS)) {
-            priv->def_leasefile = FILES[i];
-            break;
-        }
-    }
-
-    priv->dhcp_listener = g_object_ref(nm_dhcp_listener_get());
-    g_signal_connect(priv->dhcp_listener,
-                     NM_DHCP_LISTENER_EVENT,
-                     G_CALLBACK(nm_dhcp_client_handle_event),
-                     self);
-}
-
-static void
-dispose(GObject *object)
-{
-    NMDhcpDhclientPrivate *priv = NM_DHCP_DHCLIENT_GET_PRIVATE(object);
-
-    if (priv->dhcp_listener) {
-        g_signal_handlers_disconnect_by_func(priv->dhcp_listener,
-                                             G_CALLBACK(nm_dhcp_client_handle_event),
-                                             NM_DHCP_DHCLIENT(object));
-        g_clear_object(&priv->dhcp_listener);
-    }
-
-    nm_clear_g_free(&priv->pid_file);
-    nm_clear_g_free(&priv->conf_file);
-    nm_clear_g_free(&priv->lease_file);
-
-    G_OBJECT_CLASS(nm_dhcp_dhclient_parent_class)->dispose(object);
-}
-
-static void
-nm_dhcp_dhclient_class_init(NMDhcpDhclientClass *dhclient_class)
-{
-    NMDhcpClientClass *client_class = NM_DHCP_CLIENT_CLASS(dhclient_class);
-    GObjectClass      *object_class = G_OBJECT_CLASS(dhclient_class);
-
-    object_class->dispose = dispose;
-
-    client_class->ip4_start = ip4_start;
-    client_class->ip6_start = ip6_start;
-    client_class->stop      = stop;
-}
-
-const NMDhcpClientFactory _nm_dhcp_client_factory_dhclient = {
-    .name       = "dhclient",
-    .get_type_4 = nm_dhcp_dhclient_get_type,
-    .get_type_6 = nm_dhcp_dhclient_get_type,
-    .get_path   = nm_dhcp_dhclient_get_path,
-};
-
-#endif /* WITH_DHCLIENT */
diff --git a/src/core/dhcp/nm-dhcp-helper.c b/src/core/dhcp/nm-dhcp-helper.c
index 9e4cedf2..144e3cb0 100644
--- a/src/core/dhcp/nm-dhcp-helper.c
+++ b/src/core/dhcp/nm-dhcp-helper.c
@@ -205,8 +205,7 @@ do_notify:
 
     if (!NM_IN_STRSET(s_err, "org.freedesktop.DBus.Error.UnknownMethod")) {
         /* Some unexpected error. We treat that as a failure. In particular,
-         * the daemon will fail the request if ACD fails. This causes nm-dhcp-helper
-         * to fail, which in turn causes dhclient to send a DECLINE. */
+         * the daemon will fail the request if ACD fails. */
         _LOGW("failure to call notify: %s (try signal via Event)", error->message);
         success = FALSE;
         goto out;
diff --git a/src/core/dhcp/nm-dhcp-listener.c b/src/core/dhcp/nm-dhcp-listener.c
index 095131cc..cead1308 100644
--- a/src/core/dhcp/nm-dhcp-listener.c
+++ b/src/core/dhcp/nm-dhcp-listener.c
@@ -36,9 +36,6 @@ const NMDhcpClientFactory *const _nm_dhcp_manager_factories[6] = {
 #endif
     &_nm_dhcp_client_factory_systemd,
     &_nm_dhcp_client_factory_nettools,
-#if WITH_DHCLIENT
-    &_nm_dhcp_client_factory_dhclient,
-#endif
 };
 
 /*****************************************************************************/
diff --git a/src/core/dhcp/nm-dhcp-manager.c b/src/core/dhcp/nm-dhcp-manager.c
index 68bb327b..9faee84e 100644
--- a/src/core/dhcp/nm-dhcp-manager.c
+++ b/src/core/dhcp/nm-dhcp-manager.c
@@ -91,11 +91,6 @@ static const NMDhcpClientFactory *
 _client_factory_available(const NMDhcpClientFactory *client_factory)
 {
     if (client_factory) {
-        if (nm_streq(client_factory->name, "dhclient")) {
-            _LOGW(AF_UNSPEC,
-                  "attempting to used a deprecated DHCP client '%s' ",
-                  client_factory->name);
-        }
         if (!client_factory->get_path || client_factory->get_path())
             return client_factory;
     }
@@ -205,30 +200,10 @@ nm_dhcp_manager_start_client(NMDhcpManager *self, NMDhcpClientConfig *config, GE
 
     client = g_object_new(gtype, NM_DHCP_CLIENT_CONFIG, config, NULL);
 
-    /* unfortunately, our implementations work differently per address-family regarding client-id/DUID.
-     *
-     * - for IPv4, the calling code may determine a client-id (from NM's connection profile).
-     *   If present, it is taken. If not present, the DHCP plugin uses a plugin specific default.
-     *     - for "internal" plugin, the default is just "mac".
-     *     - for "dhclient", we try to get the configuration from dhclient's /etc/dhcp or fallback
-     *       to whatever dhclient uses by default.
-     *   We do it this way, because for dhclient the user may configure a default
-     *   outside of NM, and we want to honor that. Worse, dhclient could be a wapper
-     *   script where the wrapper script overwrites the client-id. We need to distinguish
-     *   between: force a particular client-id and leave it unspecified to whatever dhclient
-     *   wants.
-     *
-     * - for IPv6, the calling code always determines a client-id. It also specifies @enforce_duid,
-     *   to determine whether the given client-id must be used.
-     *     - for "internal" plugin @enforce_duid doesn't matter and the given client-id is
-     *       always used.
-     *     - for "dhclient", @enforce_duid FALSE means to first try to load the DUID from the
-     *       lease file, and only otherwise fallback to the given client-id.
-     *     - other plugins don't support DHCPv6.
-     *   It's done this way, so that existing dhclient setups don't change behavior on upgrade.
+    /* - for IPv4, the calling code may determine a client-id (from NM's connection profile).
+     *   If present, it is taken. If not present, the default is just "mac".
      *
-     * This difference is cumbersome and only exists because of "dhclient" which supports hacking the
-     * default outside of NetworkManager API.
+     * - for IPv6, the calling code always determines a client-id.
      */
 
     if (!nm_dhcp_client_start(client, error))
@@ -289,8 +264,10 @@ nm_dhcp_manager_init(NMDhcpManager *self)
                                  NM_CONFIG_GET_VALUE_STRIP | NM_CONFIG_GET_VALUE_NO_EMPTY);
     client = client_free;
     if (client) {
-        client_factory = _client_factory_available(_client_factory_find_by_name(client));
+        client_factory = _client_factory_find_by_name(client);
         if (!client_factory)
+            _LOGW(AF_UNSPEC, "init: unknown DHCP client '%s', ignoring", client);
+        else if (!(client_factory = _client_factory_available(client_factory)))
             _LOGW(AF_UNSPEC, "init: DHCP client '%s' not available", client);
     }
     if (!client_factory) {
diff --git a/src/core/dhcp/nm-dhcp-nettools.c b/src/core/dhcp/nm-dhcp-nettools.c
index 27bb136b..ad9cb893 100644
--- a/src/core/dhcp/nm-dhcp-nettools.c
+++ b/src/core/dhcp/nm-dhcp-nettools.c
@@ -269,7 +269,7 @@ lease_parse_address(NMDhcpNettools    *self /* for logging context only */,
         char str2[NM_INET_ADDRSTRLEN];
 
         /* Some DHCP servers may not set the subnet-mask (issue#1037).
-         * Do the same as the dhclient plugin and use a default. */
+         * Use a default. */
         a_plen    = nm_ip4_addr_get_default_prefix(a_address.s_addr);
         a_netmask = nm_ip4_addr_netmask_from_prefix(a_plen);
         _LOGT("missing subnet mask (option 1). Guess %s based on IP address %s",
@@ -418,7 +418,6 @@ lease_parse_routes(NDhcp4ClientLease *lease,
     in_addr_t     gateway;
     uint8_t       plen;
     guint32       m;
-    gboolean      has_router_from_classless   = FALSE;
     gboolean      has_classless               = FALSE;
     guint32       default_route_metric_offset = 0;
     const guint8 *l_data;
@@ -434,7 +433,7 @@ lease_parse_routes(NDhcp4ClientLease *lease,
      * We will however also parse one of the options into the "l3cd" for configuring routing.
      * Thereby we prefer 121 over 249 over 33.
      *
-     * Preferring 121 over 33 is defined by RFC 3443.
+     * Preferring 121 over 33 is defined by RFC 3442.
      * Preferring 121 over 249 over 33 is made up as it makes sense (the MS docs are not very clear).
      */
     for (i = 0; i < 2; i++) {
@@ -460,8 +459,7 @@ lease_parse_routes(NDhcp4ClientLease *lease,
             if (plen == 0) {
                 /* if there are multiple default routes, we add them with differing
                  * metrics. */
-                m                         = default_route_metric_offset++;
-                has_router_from_classless = TRUE;
+                m = default_route_metric_offset++;
             } else
                 m = 0;
 
@@ -495,7 +493,7 @@ lease_parse_routes(NDhcp4ClientLease *lease,
             nm_str_buf_append_printf(sbuf, "%s/%d %s", dest_str, (int) plen, gateway_str);
 
             if (has_classless) {
-                /* RFC 3443: if the DHCP server returns both a Classless Static Routes
+                /* RFC 3442: if the DHCP server returns both a Classless Static Routes
                  * option and a Static Routes option, the DHCP client MUST ignore the
                  * Static Routes option. */
                 continue;
@@ -539,13 +537,10 @@ lease_parse_routes(NDhcp4ClientLease *lease,
                 continue;
             }
 
-            if (has_router_from_classless) {
-                /* If the DHCP server returns both a Classless Static Routes option and a
-                 * Router option, the DHCP client MUST ignore the Router option [RFC 3442].
-                 *
-                 * Be more lenient and ignore the Router option only if Classless Static
-                 * Routes contain a default gateway (as other DHCP backends do).
-                 */
+            if (has_classless) {
+                /* RFC 3442: if the DHCP server returns both a Classless Static Routes
+                 * option and a Router option, the DHCP client MUST ignore the Router
+                 * option. */
                 continue;
             }
 
@@ -1475,7 +1470,9 @@ ip4_start(NMDhcpClient *client, GError **error)
             } else {
                 fqdn_len = strlen(client_config->hostname);
                 if (fqdn_len > sizeof(buffer) - 3) {
-                    nm_utils_error_set(error, r, "failed to set DHCP FQDN: name too long");
+                    nm_utils_error_set_literal(error,
+                                               NM_UTILS_ERROR_UNKNOWN,
+                                               "failed to set DHCP FQDN: name too long");
                     return FALSE;
                 }
                 memcpy(buffer + 3, client_config->hostname, fqdn_len);
@@ -1530,7 +1527,8 @@ ip4_start(NMDhcpClient *client, GError **error)
 
     _LOGT("dhcp-client4: start " NM_HASH_OBFUSCATE_PTR_FMT, NM_HASH_OBFUSCATE_PTR(priv->client));
 
-    nm_dhcp_client_set_effective_client_id(client, effective_client_id);
+    if (effective_client_id)
+        nm_dhcp_client_set_effective_client_id(client, effective_client_id);
 
     return TRUE;
 }
diff --git a/src/core/dhcp/nm-dhcp-systemd.c b/src/core/dhcp/nm-dhcp-systemd.c
index b570f7e5..547e730b 100644
--- a/src/core/dhcp/nm-dhcp-systemd.c
+++ b/src/core/dhcp/nm-dhcp-systemd.c
@@ -92,6 +92,7 @@ lease_to_ip6_config(NMDhcpSystemd *self, sd_dhcp6_lease *lease, gint32 ts, GErro
     const char                             *s;
     nm_auto_free_gstring GString           *str = NULL;
     int                                     num, i;
+    gboolean                                has_any_prefix_delegated = FALSE;
 
     nm_assert(lease);
 
@@ -107,6 +108,28 @@ lease_to_ip6_config(NMDhcpSystemd *self, sd_dhcp6_lease *lease, gint32 ts, GErro
                               NM_DHCP_OPTION_DHCP6_NM_IAID,
                               nm_dhcp_iaid_to_hexstr(config->v6.iaid, iaid_buf));
 
+    {
+        struct in6_addr prefix;
+        uint8_t         prefix_len;
+
+        nm_gstring_prepare(&str);
+        sd_dhcp6_lease_pd_iterator_reset(lease);
+        while (!sd_dhcp6_lease_get_pd_prefix(lease, &prefix, &prefix_len)) {
+            nm_gstring_add_space_delimiter(str);
+            nm_inet6_ntop(&prefix, addr_str);
+            g_string_append_printf(str, "%s/%u", addr_str, prefix_len);
+            sd_dhcp6_lease_pd_iterator_next(lease);
+        }
+        if (str->len > 0) {
+            nm_dhcp_option_add_option(options,
+                                      TRUE,
+                                      AF_INET6,
+                                      NM_DHCP_OPTION_DHCP6_IA_PD,
+                                      str->str);
+            has_any_prefix_delegated = TRUE;
+        }
+    }
+
     if (!config->v6.info_only) {
         gboolean has_any_addresses = FALSE;
         uint64_t lft_pref;
@@ -142,11 +165,11 @@ lease_to_ip6_config(NMDhcpSystemd *self, sd_dhcp6_lease *lease, gint32 ts, GErro
                                       str->str);
         }
 
-        if (!has_any_addresses) {
+        if (!has_any_addresses && !has_any_prefix_delegated) {
             g_set_error_literal(error,
                                 NM_MANAGER_ERROR,
                                 NM_MANAGER_ERROR_FAILED,
-                                "no address received in managed mode");
+                                "no address or prefix delegation received in managed mode");
             return NULL;
         }
     }
@@ -166,27 +189,6 @@ lease_to_ip6_config(NMDhcpSystemd *self, sd_dhcp6_lease *lease, gint32 ts, GErro
                                   str->str);
     }
 
-    {
-        struct in6_addr prefix;
-        uint8_t         prefix_len;
-
-        nm_gstring_prepare(&str);
-        sd_dhcp6_lease_pd_iterator_reset(lease);
-        while (!sd_dhcp6_lease_get_pd_prefix(lease, &prefix, &prefix_len)) {
-            nm_gstring_add_space_delimiter(str);
-            nm_inet6_ntop(&prefix, addr_str);
-            g_string_append_printf(str, "%s/%u", addr_str, prefix_len);
-            sd_dhcp6_lease_pd_iterator_next(lease);
-        }
-        if (str->len > 0) {
-            nm_dhcp_option_add_option(options,
-                                      TRUE,
-                                      AF_INET6,
-                                      NM_DHCP_OPTION_DHCP6_IA_PD,
-                                      str->str);
-        }
-    }
-
     num = sd_dhcp6_lease_get_domains(lease, &domains);
     if (num > 0) {
         nm_gstring_prepare(&str);
diff --git a/src/core/dhcp/nm-dhcp-utils.c b/src/core/dhcp/nm-dhcp-utils.c
index 8a9bd58c..8c3001f2 100644
--- a/src/core/dhcp/nm-dhcp-utils.c
+++ b/src/core/dhcp/nm-dhcp-utils.c
@@ -827,64 +827,6 @@ nm_dhcp_utils_get_leasefile_path(int         addr_family,
     return FALSE;
 }
 
-gboolean
-nm_dhcp_utils_merge_new_dhcp6_lease(const NML3ConfigData  *l3cd_old,
-                                    const NML3ConfigData  *l3cd_new,
-                                    const NML3ConfigData **out_l3cd_merged)
-{
-    nm_auto_unref_l3cd_init NML3ConfigData *l3cd_merged = NULL;
-    const NMPlatformIP6Address             *addr;
-    NMDhcpLease                            *lease_old;
-    NMDhcpLease                            *lease_new;
-    NMDedupMultiIter                        iter;
-    const char                             *start;
-    const char                             *iaid;
-
-    nm_assert(out_l3cd_merged && !*out_l3cd_merged);
-
-    if (!l3cd_old)
-        return FALSE;
-    if (!l3cd_new)
-        return FALSE;
-
-    lease_new = nm_l3_config_data_get_dhcp_lease(l3cd_new, AF_INET6);
-    if (!lease_new)
-        return FALSE;
-
-    lease_old = nm_l3_config_data_get_dhcp_lease(l3cd_old, AF_INET6);
-    if (!lease_old)
-        return FALSE;
-
-    start = nm_dhcp_lease_lookup_option(lease_new, "life_starts");
-    if (!start)
-        return FALSE;
-    iaid = nm_dhcp_lease_lookup_option(lease_new, "iaid");
-    if (!iaid)
-        return FALSE;
-
-    if (!nm_streq0(start, nm_dhcp_lease_lookup_option(lease_old, "life_starts")))
-        return FALSE;
-    if (!nm_streq0(iaid, nm_dhcp_lease_lookup_option(lease_old, "iaid")))
-        return FALSE;
-
-    /* If the server sends multiple IPv6 addresses, we receive a state
-     * changed event for each of them. Use the event ID to merge IPv6
-     * addresses from the same transaction into a single configuration.
-     **/
-
-    l3cd_merged = nm_l3_config_data_new_clone(l3cd_old, 0);
-
-    nm_l3_config_data_iter_ip6_address_for_each (&iter, l3cd_new, &addr)
-        nm_l3_config_data_add_address_6(l3cd_merged, addr);
-
-    /* FIXME(l3cfg): Note that we keep the original NMDhcpLease. All we take from the new lease are the
-     * addresses. Maybe this is not right and we should merge the leases too?? */
-    nm_l3_config_data_set_dhcp_lease(l3cd_merged, AF_INET6, lease_old);
-
-    *out_l3cd_merged = nm_l3_config_data_ref_and_seal(g_steal_pointer(&l3cd_merged));
-    return TRUE;
-}
-
 /*****************************************************************************/
 
 void
diff --git a/src/core/dhcp/nm-dhcp-utils.h b/src/core/dhcp/nm-dhcp-utils.h
index 99898524..a2dde94e 100644
--- a/src/core/dhcp/nm-dhcp-utils.h
+++ b/src/core/dhcp/nm-dhcp-utils.h
@@ -33,12 +33,6 @@ gboolean nm_dhcp_utils_get_leasefile_path(int         addr_family,
                                           const char *uuid,
                                           char      **out_leasefile_path);
 
-char *nm_dhcp_utils_get_dhcp6_event_id(GHashTable *lease);
-
-gboolean nm_dhcp_utils_merge_new_dhcp6_lease(const NML3ConfigData  *l3cd_old,
-                                             const NML3ConfigData  *l3cd_new,
-                                             const NML3ConfigData **out_l3cd_merged);
-
 /*****************************************************************************/
 
 static inline gboolean
diff --git a/src/core/dhcp/tests/meson.build b/src/core/dhcp/tests/meson.build
index e43c8cab..8b2a7b76 100644
--- a/src/core/dhcp/tests/meson.build
+++ b/src/core/dhcp/tests/meson.build
@@ -1,7 +1,6 @@
 # SPDX-License-Identifier: LGPL-2.1-or-later
 
 test_units = [
-  'test-dhcp-dhclient',
   'test-dhcp-utils',
 ]
 
diff --git a/src/core/dhcp/tests/test-dhclient-commented-duid.leases b/src/core/dhcp/tests/test-dhclient-commented-duid.leases
deleted file mode 100644
index 3e46ae7d..00000000
--- a/src/core/dhcp/tests/test-dhclient-commented-duid.leases
+++ /dev/null
@@ -1,2 +0,0 @@
-#default-duid "\000\001\000\001\030y\246\023`g \354Lp";
-
diff --git a/src/core/dhcp/tests/test-dhclient-duid.leases b/src/core/dhcp/tests/test-dhclient-duid.leases
deleted file mode 100644
index 229331d4..00000000
--- a/src/core/dhcp/tests/test-dhclient-duid.leases
+++ /dev/null
@@ -1,2 +0,0 @@
-default-duid "\000\001\000\001\030y\246\023`g \354Lp";
-
diff --git a/src/core/dhcp/tests/test-dhcp-dhclient.c b/src/core/dhcp/tests/test-dhcp-dhclient.c
deleted file mode 100644
index 6a7b7185..00000000
--- a/src/core/dhcp/tests/test-dhcp-dhclient.c
+++ /dev/null
@@ -1,1478 +0,0 @@
-/* SPDX-License-Identifier: GPL-2.0-or-later */
-/*
- * Copyright (C) 2010 Red Hat, Inc.
- */
-
-#include "src/core/nm-default-daemon.h"
-
-#include <unistd.h>
-#include <arpa/inet.h>
-#include <linux/rtnetlink.h>
-
-#include "libnm-glib-aux/nm-dedup-multi.h"
-
-#include "NetworkManagerUtils.h"
-#include "dhcp/nm-dhcp-dhclient-utils.h"
-#include "dhcp/nm-dhcp-utils.h"
-#include "nm-utils.h"
-#include "libnm-platform/nm-platform.h"
-
-#include "nm-test-utils-core.h"
-
-#define TEST_DIR    NM_BUILD_SRCDIR "/src/core/dhcp/tests"
-#define TEST_MUDURL "https://example.com/mud.json"
-
-static void
-test_config(const char         *orig,
-            const char         *expected,
-            int                 addr_family,
-            const char         *hostname,
-            guint32             timeout,
-            gboolean            use_fqdn,
-            NMDhcpHostnameFlags hostname_flags,
-            const char         *dhcp_client_id,
-            GBytes             *expected_new_client_id,
-            const char         *iface,
-            const char         *anycast_addr,
-            const char         *mud_url)
-{
-    gs_free char *new                     = NULL;
-    gs_unref_bytes GBytes *client_id      = NULL;
-    gs_unref_bytes GBytes *new_client_id  = NULL;
-    gboolean               send_client_id = TRUE;
-
-    if (nm_streq0(dhcp_client_id, "none")) {
-        send_client_id = FALSE;
-    } else if (dhcp_client_id) {
-        client_id = nm_dhcp_utils_client_id_string_to_bytes(dhcp_client_id);
-        g_assert(client_id);
-    }
-
-    new = nm_dhcp_dhclient_create_config(iface,
-                                         addr_family,
-                                         client_id,
-                                         send_client_id,
-                                         anycast_addr,
-                                         hostname,
-                                         timeout,
-                                         use_fqdn,
-                                         hostname_flags,
-                                         mud_url,
-                                         NULL,
-                                         "/path/to/dhclient.conf",
-                                         orig,
-                                         &new_client_id);
-    g_assert(new != NULL);
-
-    if (!nm_streq(new, expected)) {
-        g_message("\n* OLD ---------------------------------\n"
-                  "%s"
-                  "\n- NEW -----------------------------------\n"
-                  "%s"
-                  "\n+ EXPECTED ++++++++++++++++++++++++++++++\n"
-                  "%s"
-                  "\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n",
-                  orig,
-                  new,
-                  expected);
-    }
-    g_assert_cmpstr(new, ==, expected);
-
-    if (expected_new_client_id) {
-        g_assert(new_client_id);
-        g_assert(g_bytes_equal(new_client_id, expected_new_client_id));
-    } else
-        g_assert(new_client_id == NULL);
-}
-
-/*****************************************************************************/
-
-static const char *orig_missing_expected =
-    "# Created by NetworkManager\n"
-    "\n\n"
-    "option rfc3442-classless-static-routes code 121 = array of unsigned integer 8;\n"
-    "option ms-classless-static-routes code 249 = array of unsigned integer 8;\n"
-    "option wpad code 252 = string;\n"
-    "\n"
-    "also request rfc3442-classless-static-routes;\n"
-    "also request ms-classless-static-routes;\n"
-    "also request static-routes;\n"
-    "also request wpad;\n"
-    "also request ntp-servers;\n"
-    "also request root-path;\n"
-    "\n";
-
-static void
-test_orig_missing(void)
-{
-    test_config(NULL,
-                orig_missing_expected,
-                AF_INET,
-                NULL,
-                0,
-                FALSE,
-                NM_DHCP_HOSTNAME_FLAG_NONE,
-                NULL,
-                NULL,
-                "eth0",
-                NULL,
-                NULL);
-}
-
-/*****************************************************************************/
-
-static const char *orig_missing_add_mud_url_expected =
-    "# Created by NetworkManager\n"
-    "\n"
-    "option mudurl code 161 = text;\n"
-    "send mudurl \"https://example.com/mud.json\";\n\n"
-    "option rfc3442-classless-static-routes code 121 = array of unsigned integer 8;\n"
-    "option ms-classless-static-routes code 249 = array of unsigned integer 8;\n"
-    "option wpad code 252 = string;\n"
-    "\n"
-    "also request rfc3442-classless-static-routes;\n"
-    "also request ms-classless-static-routes;\n"
-    "also request static-routes;\n"
-    "also request wpad;\n"
-    "also request ntp-servers;\n"
-    "also request root-path;\n"
-    "\n";
-
-static void
-test_orig_missing_add_mud_url(void)
-{
-    test_config(NULL,
-                orig_missing_add_mud_url_expected,
-                AF_INET,
-                NULL,
-                0,
-                FALSE,
-                NM_DHCP_HOSTNAME_FLAG_NONE,
-                NULL,
-                NULL,
-                "eth0",
-                NULL,
-                TEST_MUDURL);
-}
-
-/*****************************************************************************/
-
-static const char *override_client_id_orig = "send dhcp-client-identifier 00:30:04:20:7A:08;\n";
-
-static const char *override_client_id_expected =
-    "# Created by NetworkManager\n"
-    "# Merged from /path/to/dhclient.conf\n"
-    "\n"
-    "send dhcp-client-identifier 11:22:33:44:55:66; # added by NetworkManager\n"
-    "\n"
-    "option rfc3442-classless-static-routes code 121 = array of unsigned integer 8;\n"
-    "option ms-classless-static-routes code 249 = array of unsigned integer 8;\n"
-    "option wpad code 252 = string;\n"
-    "\n"
-    "also request rfc3442-classless-static-routes;\n"
-    "also request ms-classless-static-routes;\n"
-    "also request static-routes;\n"
-    "also request wpad;\n"
-    "also request ntp-servers;\n"
-    "also request root-path;\n"
-    "\n";
-
-static void
-test_override_client_id(void)
-{
-    test_config(override_client_id_orig,
-                override_client_id_expected,
-                AF_INET,
-                NULL,
-                0,
-                FALSE,
-                NM_DHCP_HOSTNAME_FLAG_NONE,
-                "11:22:33:44:55:66",
-                NULL,
-                "eth0",
-                NULL,
-                NULL);
-}
-
-/*****************************************************************************/
-
-static const char *quote_client_id_expected =
-    "# Created by NetworkManager\n"
-    "\n"
-    "send dhcp-client-identifier \"\\x00abcd\"; # added by NetworkManager\n"
-    "\n"
-    "option rfc3442-classless-static-routes code 121 = array of unsigned integer 8;\n"
-    "option ms-classless-static-routes code 249 = array of unsigned integer 8;\n"
-    "option wpad code 252 = string;\n"
-    "\n"
-    "also request rfc3442-classless-static-routes;\n"
-    "also request ms-classless-static-routes;\n"
-    "also request static-routes;\n"
-    "also request wpad;\n"
-    "also request ntp-servers;\n"
-    "also request root-path;\n"
-    "\n";
-
-static void
-test_quote_client_id(void)
-{
-    test_config(NULL,
-                quote_client_id_expected,
-                AF_INET,
-                NULL,
-                0,
-                FALSE,
-                NM_DHCP_HOSTNAME_FLAG_NONE,
-                "abcd",
-                NULL,
-                "eth0",
-                NULL,
-                NULL);
-}
-
-/*****************************************************************************/
-
-static const char *quote_client_id_expected_2 =
-    "# Created by NetworkManager\n"
-    "\n"
-    "send dhcp-client-identifier 00:61:5c:62:63; # added by NetworkManager\n"
-    "\n"
-    "option rfc3442-classless-static-routes code 121 = array of unsigned integer 8;\n"
-    "option ms-classless-static-routes code 249 = array of unsigned integer 8;\n"
-    "option wpad code 252 = string;\n"
-    "\n"
-    "also request rfc3442-classless-static-routes;\n"
-    "also request ms-classless-static-routes;\n"
-    "also request static-routes;\n"
-    "also request wpad;\n"
-    "also request ntp-servers;\n"
-    "also request root-path;\n"
-    "\n";
-
-static void
-test_quote_client_id_2(void)
-{
-    test_config(NULL,
-                quote_client_id_expected_2,
-                AF_INET,
-                NULL,
-                0,
-                FALSE,
-                NM_DHCP_HOSTNAME_FLAG_NONE,
-                "a\\bc",
-                NULL,
-                "eth0",
-                NULL,
-                NULL);
-}
-
-/*****************************************************************************/
-
-static const char *hex_zero_client_id_expected =
-    "# Created by NetworkManager\n"
-    "\n"
-    "send dhcp-client-identifier 00:11:22:33; # added by NetworkManager\n"
-    "\n"
-    "option rfc3442-classless-static-routes code 121 = array of unsigned integer 8;\n"
-    "option ms-classless-static-routes code 249 = array of unsigned integer 8;\n"
-    "option wpad code 252 = string;\n"
-    "\n"
-    "also request rfc3442-classless-static-routes;\n"
-    "also request ms-classless-static-routes;\n"
-    "also request static-routes;\n"
-    "also request wpad;\n"
-    "also request ntp-servers;\n"
-    "also request root-path;\n"
-    "\n";
-
-static void
-test_hex_zero_client_id(void)
-{
-    test_config(NULL,
-                hex_zero_client_id_expected,
-                AF_INET,
-                NULL,
-                0,
-                FALSE,
-                NM_DHCP_HOSTNAME_FLAG_NONE,
-                "00:11:22:33",
-                NULL,
-                "eth0",
-                NULL,
-                NULL);
-}
-
-/*****************************************************************************/
-
-static const char *ascii_client_id_expected =
-    "# Created by NetworkManager\n"
-    "\n"
-    "send dhcp-client-identifier \"\\x00qb:cd:ef:12:34:56\"; # added by NetworkManager\n"
-    "\n"
-    "option rfc3442-classless-static-routes code 121 = array of unsigned integer 8;\n"
-    "option ms-classless-static-routes code 249 = array of unsigned integer 8;\n"
-    "option wpad code 252 = string;\n"
-    "\n"
-    "also request rfc3442-classless-static-routes;\n"
-    "also request ms-classless-static-routes;\n"
-    "also request static-routes;\n"
-    "also request wpad;\n"
-    "also request ntp-servers;\n"
-    "also request root-path;\n"
-    "\n";
-
-static void
-test_ascii_client_id(void)
-{
-    test_config(NULL,
-                ascii_client_id_expected,
-                AF_INET,
-                NULL,
-                0,
-                FALSE,
-                NM_DHCP_HOSTNAME_FLAG_NONE,
-                "qb:cd:ef:12:34:56",
-                NULL,
-                "eth0",
-                NULL,
-                NULL);
-}
-
-/*****************************************************************************/
-
-static const char *hex_single_client_id_expected =
-    "# Created by NetworkManager\n"
-    "\n"
-    "send dhcp-client-identifier ab:cd:0e:12:34:56; # added by NetworkManager\n"
-    "\n"
-    "option rfc3442-classless-static-routes code 121 = array of unsigned integer 8;\n"
-    "option ms-classless-static-routes code 249 = array of unsigned integer 8;\n"
-    "option wpad code 252 = string;\n"
-    "\n"
-    "also request rfc3442-classless-static-routes;\n"
-    "also request ms-classless-static-routes;\n"
-    "also request static-routes;\n"
-    "also request wpad;\n"
-    "also request ntp-servers;\n"
-    "also request root-path;\n"
-    "\n";
-
-static void
-test_hex_single_client_id(void)
-{
-    test_config(NULL,
-                hex_single_client_id_expected,
-                AF_INET,
-                NULL,
-                0,
-                FALSE,
-                NM_DHCP_HOSTNAME_FLAG_NONE,
-                "ab:cd:e:12:34:56",
-                NULL,
-                "eth0",
-                NULL,
-                NULL);
-}
-
-/*****************************************************************************/
-
-static const char *existing_hex_client_id_orig = "send dhcp-client-identifier 10:30:04:20:7A:08;\n";
-
-static const char *existing_hex_client_id_expected =
-    "# Created by NetworkManager\n"
-    "# Merged from /path/to/dhclient.conf\n"
-    "\n"
-    "send dhcp-client-identifier 10:30:04:20:7A:08;\n"
-    "\n"
-    "option rfc3442-classless-static-routes code 121 = array of unsigned integer 8;\n"
-    "option ms-classless-static-routes code 249 = array of unsigned integer 8;\n"
-    "option wpad code 252 = string;\n"
-    "\n"
-    "also request rfc3442-classless-static-routes;\n"
-    "also request ms-classless-static-routes;\n"
-    "also request static-routes;\n"
-    "also request wpad;\n"
-    "also request ntp-servers;\n"
-    "also request root-path;\n"
-    "\n";
-
-static void
-test_existing_hex_client_id(void)
-{
-    gs_unref_bytes GBytes *new_client_id = NULL;
-    const guint8           bytes[]       = {0x10, 0x30, 0x04, 0x20, 0x7A, 0x08};
-
-    new_client_id = g_bytes_new(bytes, sizeof(bytes));
-    test_config(existing_hex_client_id_orig,
-                existing_hex_client_id_expected,
-                AF_INET,
-                NULL,
-                0,
-                FALSE,
-                NM_DHCP_HOSTNAME_FLAG_NONE,
-                NULL,
-                new_client_id,
-                "eth0",
-                NULL,
-                NULL);
-}
-
-/*****************************************************************************/
-
-static const char *existing_escaped_client_id_orig =
-    "send dhcp-client-identifier \"\\044test\\xfe\";\n";
-
-static const char *existing_escaped_client_id_expected =
-    "# Created by NetworkManager\n"
-    "# Merged from /path/to/dhclient.conf\n"
-    "\n"
-    "send dhcp-client-identifier \"\\044test\\xfe\";\n"
-    "\n"
-    "option rfc3442-classless-static-routes code 121 = array of unsigned integer 8;\n"
-    "option ms-classless-static-routes code 249 = array of unsigned integer 8;\n"
-    "option wpad code 252 = string;\n"
-    "\n"
-    "also request rfc3442-classless-static-routes;\n"
-    "also request ms-classless-static-routes;\n"
-    "also request static-routes;\n"
-    "also request wpad;\n"
-    "also request ntp-servers;\n"
-    "also request root-path;\n"
-    "\n";
-
-static void
-test_existing_escaped_client_id(void)
-{
-    gs_unref_bytes GBytes *new_client_id = NULL;
-
-    new_client_id = g_bytes_new("$test\xfe", 6);
-    test_config(existing_escaped_client_id_orig,
-                existing_escaped_client_id_expected,
-                AF_INET,
-                NULL,
-                0,
-                FALSE,
-                NM_DHCP_HOSTNAME_FLAG_NONE,
-                NULL,
-                new_client_id,
-                "eth0",
-                NULL,
-                NULL);
-}
-
-/*****************************************************************************/
-
-#define EACID "qb:cd:ef:12:34:56"
-
-static const char *existing_ascii_client_id_orig =
-    "send dhcp-client-identifier \"\\x00" EACID "\";\n";
-
-static const char *existing_ascii_client_id_expected =
-    "# Created by NetworkManager\n"
-    "# Merged from /path/to/dhclient.conf\n"
-    "\n"
-    "send dhcp-client-identifier \"\\x00" EACID "\";\n"
-    "\n"
-    "option rfc3442-classless-static-routes code 121 = array of unsigned integer 8;\n"
-    "option ms-classless-static-routes code 249 = array of unsigned integer 8;\n"
-    "option wpad code 252 = string;\n"
-    "\n"
-    "also request rfc3442-classless-static-routes;\n"
-    "also request ms-classless-static-routes;\n"
-    "also request static-routes;\n"
-    "also request wpad;\n"
-    "also request ntp-servers;\n"
-    "also request root-path;\n"
-    "\n";
-
-static void
-test_existing_ascii_client_id(void)
-{
-    gs_unref_bytes GBytes *new_client_id             = NULL;
-    char                   buf[NM_STRLEN(EACID) + 1] = {0};
-
-    memcpy(buf + 1, EACID, NM_STRLEN(EACID));
-    new_client_id = g_bytes_new(buf, sizeof(buf));
-    test_config(existing_ascii_client_id_orig,
-                existing_ascii_client_id_expected,
-                AF_INET,
-                NULL,
-                0,
-                FALSE,
-                NM_DHCP_HOSTNAME_FLAG_NONE,
-                NULL,
-                new_client_id,
-                "eth0",
-                NULL,
-                NULL);
-}
-
-/*****************************************************************************/
-
-static const char *none_client_id_orig = "send dhcp-client-identifier 10:30:04:20:7A:08;\n";
-
-static const char *none_client_id_expected =
-    "# Created by NetworkManager\n"
-    "# Merged from /path/to/dhclient.conf\n"
-    "\n"
-    "send dhcp-client-identifier \"\"; # added by NetworkManager\n"
-    "\n"
-    "option rfc3442-classless-static-routes code 121 = array of unsigned integer 8;\n"
-    "option ms-classless-static-routes code 249 = array of unsigned integer 8;\n"
-    "option wpad code 252 = string;\n"
-    "\n"
-    "also request rfc3442-classless-static-routes;\n"
-    "also request ms-classless-static-routes;\n"
-    "also request static-routes;\n"
-    "also request wpad;\n"
-    "also request ntp-servers;\n"
-    "also request root-path;\n"
-    "\n";
-
-static void
-test_none_client_id(void)
-{
-    const char            *connection_client_id = "none";
-    gs_unref_bytes GBytes *expected_client_id   = NULL;
-
-    test_config(none_client_id_orig,
-                none_client_id_expected,
-                AF_INET,
-                NULL,
-                0,
-                FALSE,
-                NM_DHCP_HOSTNAME_FLAG_NONE,
-                connection_client_id,
-                expected_client_id,
-                "eth0",
-                NULL,
-                NULL);
-}
-
-/*****************************************************************************/
-
-static const char *missing_client_id_orig = "";
-
-static const char *missing_client_id_expected =
-    "# Created by NetworkManager\n"
-    "# Merged from /path/to/dhclient.conf\n"
-    "\n"
-    "\n"
-    "option rfc3442-classless-static-routes code 121 = array of unsigned integer 8;\n"
-    "option ms-classless-static-routes code 249 = array of unsigned integer 8;\n"
-    "option wpad code 252 = string;\n"
-    "\n"
-    "also request rfc3442-classless-static-routes;\n"
-    "also request ms-classless-static-routes;\n"
-    "also request static-routes;\n"
-    "also request wpad;\n"
-    "also request ntp-servers;\n"
-    "also request root-path;\n"
-    "\n";
-
-static void
-test_missing_client_id(void)
-{
-    const char            *connection_client_id = NULL;
-    gs_unref_bytes GBytes *expected_client_id   = NULL;
-
-    test_config(missing_client_id_orig,
-                missing_client_id_expected,
-                AF_INET,
-                NULL,
-                0,
-                FALSE,
-                NM_DHCP_HOSTNAME_FLAG_NONE,
-                connection_client_id,
-                expected_client_id,
-                "eth0",
-                NULL,
-                NULL);
-}
-
-/*****************************************************************************/
-
-static const char *fqdn_expected =
-    "# Created by NetworkManager\n"
-    "\n"
-    "send fqdn.fqdn \"foo.bar.com\"; # added by NetworkManager\n"
-    "send fqdn.encoded on;\n"
-    "send fqdn.server-update off;\n"
-    "send fqdn.no-client-update on;\n"
-    "\n"
-    "option rfc3442-classless-static-routes code 121 = array of unsigned integer 8;\n"
-    "option ms-classless-static-routes code 249 = array of unsigned integer 8;\n"
-    "option wpad code 252 = string;\n"
-    "\n"
-    "also request rfc3442-classless-static-routes;\n"
-    "also request ms-classless-static-routes;\n"
-    "also request static-routes;\n"
-    "also request wpad;\n"
-    "also request ntp-servers;\n"
-    "also request root-path;\n\n";
-
-static void
-test_fqdn(void)
-{
-    test_config(NULL,
-                fqdn_expected,
-                AF_INET,
-                "foo.bar.com",
-                0,
-                TRUE,
-                NM_DHCP_HOSTNAME_FLAG_FQDN_ENCODED | NM_DHCP_HOSTNAME_FLAG_FQDN_NO_UPDATE,
-                NULL,
-                NULL,
-                "eth0",
-                NULL,
-                NULL);
-}
-
-static const char *fqdn_options_override_orig =
-    "\n"
-    "send fqdn.fqdn \"foobar.com\"\n" /* NM must ignore this ... */
-    "send fqdn.encoded off;\n"        /* ... and honor these */
-    "send fqdn.server-update off;\n";
-
-static const char *fqdn_options_override_expected =
-    "# Created by NetworkManager\n"
-    "# Merged from /path/to/dhclient.conf\n"
-    "\n"
-    "send fqdn.fqdn \"example2.com\"; # added by NetworkManager\n"
-    "send fqdn.encoded off;\n"
-    "send fqdn.server-update on;\n"
-    "send fqdn.no-client-update off;\n"
-    "\n"
-    "option rfc3442-classless-static-routes code 121 = array of unsigned integer 8;\n"
-    "option ms-classless-static-routes code 249 = array of unsigned integer 8;\n"
-    "option wpad code 252 = string;\n"
-    "\n"
-    "also request rfc3442-classless-static-routes;\n"
-    "also request ms-classless-static-routes;\n"
-    "also request static-routes;\n"
-    "also request wpad;\n"
-    "also request ntp-servers;\n"
-    "also request root-path;\n"
-    "\n"
-    "# FQDN options from /path/to/dhclient.conf\n"
-    "send fqdn.encoded off;\n"
-    "send fqdn.server-update off;\n\n";
-
-static void
-test_fqdn_options_override(void)
-{
-    test_config(fqdn_options_override_orig,
-                fqdn_options_override_expected,
-                AF_INET,
-                "example2.com",
-                0,
-                NM_DHCP_HOSTNAME_FLAG_FQDN_SERV_UPDATE,
-                TRUE,
-                NULL,
-                NULL,
-                "eth0",
-                NULL,
-                NULL);
-}
-
-/*****************************************************************************/
-
-static const char *override_hostname_orig = "send host-name \"foobar\";\n";
-
-static const char *override_hostname_expected =
-    "# Created by NetworkManager\n"
-    "# Merged from /path/to/dhclient.conf\n"
-    "\n"
-    "send host-name \"blahblah\"; # added by NetworkManager\n"
-    "\n"
-    "option rfc3442-classless-static-routes code 121 = array of unsigned integer 8;\n"
-    "option ms-classless-static-routes code 249 = array of unsigned integer 8;\n"
-    "option wpad code 252 = string;\n"
-    "\n"
-    "also request rfc3442-classless-static-routes;\n"
-    "also request ms-classless-static-routes;\n"
-    "also request static-routes;\n"
-    "also request wpad;\n"
-    "also request ntp-servers;\n"
-    "also request root-path;\n"
-    "\n";
-
-static void
-test_override_hostname(void)
-{
-    test_config(override_hostname_orig,
-                override_hostname_expected,
-                AF_INET,
-                "blahblah",
-                0,
-                FALSE,
-                NM_DHCP_HOSTNAME_FLAG_NONE,
-                NULL,
-                NULL,
-                "eth0",
-                NULL,
-                NULL);
-}
-
-/*****************************************************************************/
-
-static const char *override_hostname6_orig = "send fqdn.fqdn \"foobar\";\n";
-
-static const char *override_hostname6_expected =
-    "# Created by NetworkManager\n"
-    "# Merged from /path/to/dhclient.conf\n"
-    "\n"
-    "send fqdn.fqdn \"blahblah.local\"; # added by NetworkManager\n"
-    "send fqdn.server-update on;\n"
-    "\n"
-    "also request dhcp6.name-servers;\n"
-    "also request dhcp6.domain-search;\n"
-    "also request dhcp6.client-id;\n"
-    "\n";
-
-static void
-test_override_hostname6(void)
-{
-    test_config(override_hostname6_orig,
-                override_hostname6_expected,
-                AF_INET6,
-                "blahblah.local",
-                0,
-                TRUE,
-                NM_DHCP_HOSTNAME_FLAG_FQDN_SERV_UPDATE,
-                NULL,
-                NULL,
-                "eth0",
-                NULL,
-                NULL);
-}
-
-/*****************************************************************************/
-
-static const char *nonfqdn_hostname6_expected =
-    "# Created by NetworkManager\n"
-    "\n"
-    "send fqdn.fqdn \"blahblah\"; # added by NetworkManager\n"
-    "send fqdn.no-client-update on;\n"
-    "\n"
-    "also request dhcp6.name-servers;\n"
-    "also request dhcp6.domain-search;\n"
-    "also request dhcp6.client-id;\n"
-    "\n";
-
-static void
-test_nonfqdn_hostname6(void)
-{
-    /* Non-FQDN hostname can now be used with dhclient */
-    test_config(NULL,
-                nonfqdn_hostname6_expected,
-                AF_INET6,
-                "blahblah",
-                0,
-                TRUE,
-                NM_DHCP_HOSTNAME_FLAG_FQDN_NO_UPDATE,
-                NULL,
-                NULL,
-                "eth0",
-                NULL,
-                NULL);
-}
-
-/*****************************************************************************/
-
-static const char *existing_alsoreq_orig = "also request something;\n"
-                                           "also request another-thing;\n";
-
-static const char *existing_alsoreq_expected =
-    "# Created by NetworkManager\n"
-    "# Merged from /path/to/dhclient.conf\n"
-    "\n\n"
-    "option rfc3442-classless-static-routes code 121 = array of unsigned integer 8;\n"
-    "option ms-classless-static-routes code 249 = array of unsigned integer 8;\n"
-    "option wpad code 252 = string;\n"
-    "\n"
-    "also request something;\n"
-    "also request another-thing;\n"
-    "also request rfc3442-classless-static-routes;\n"
-    "also request ms-classless-static-routes;\n"
-    "also request static-routes;\n"
-    "also request wpad;\n"
-    "also request ntp-servers;\n"
-    "also request root-path;\n"
-    "\n";
-
-static void
-test_existing_alsoreq(void)
-{
-    test_config(existing_alsoreq_orig,
-                existing_alsoreq_expected,
-                AF_INET,
-                NULL,
-                0,
-                FALSE,
-                NM_DHCP_HOSTNAME_FLAG_NONE,
-                NULL,
-                NULL,
-                "eth0",
-                NULL,
-                NULL);
-}
-
-/*****************************************************************************/
-
-static const char *existing_req_orig = "request something;\n"
-                                       "also request some-other-thing;\n"
-                                       "request another-thing;\n"
-                                       "also request yet-another-thing;\n";
-
-static const char *existing_req_expected =
-    "# Created by NetworkManager\n"
-    "# Merged from /path/to/dhclient.conf\n"
-    "\n\n"
-    "option rfc3442-classless-static-routes code 121 = array of unsigned integer 8;\n"
-    "option ms-classless-static-routes code 249 = array of unsigned integer 8;\n"
-    "option wpad code 252 = string;\n"
-    "\n"
-    "request; # override dhclient defaults\n"
-    "also request another-thing;\n"
-    "also request yet-another-thing;\n"
-    "also request rfc3442-classless-static-routes;\n"
-    "also request ms-classless-static-routes;\n"
-    "also request static-routes;\n"
-    "also request wpad;\n"
-    "also request ntp-servers;\n"
-    "also request root-path;\n"
-    "\n";
-
-static void
-test_existing_req(void)
-{
-    test_config(existing_req_orig,
-                existing_req_expected,
-                AF_INET,
-                NULL,
-                0,
-                FALSE,
-                NM_DHCP_HOSTNAME_FLAG_NONE,
-                NULL,
-                NULL,
-                "eth0",
-                NULL,
-                NULL);
-}
-
-/*****************************************************************************/
-
-static const char *existing_multiline_alsoreq_orig =
-    "also request something another-thing yet-another-thing\n"
-    "    foobar baz blah;\n";
-
-static const char *existing_multiline_alsoreq_expected =
-    "# Created by NetworkManager\n"
-    "# Merged from /path/to/dhclient.conf\n"
-    "\n\n"
-    "option rfc3442-classless-static-routes code 121 = array of unsigned integer 8;\n"
-    "option ms-classless-static-routes code 249 = array of unsigned integer 8;\n"
-    "option wpad code 252 = string;\n"
-    "\n"
-    "also request something;\n"
-    "also request another-thing;\n"
-    "also request yet-another-thing;\n"
-    "also request foobar;\n"
-    "also request baz;\n"
-    "also request blah;\n"
-    "also request rfc3442-classless-static-routes;\n"
-    "also request ms-classless-static-routes;\n"
-    "also request static-routes;\n"
-    "also request wpad;\n"
-    "also request ntp-servers;\n"
-    "also request root-path;\n"
-    "\n";
-
-static void
-test_existing_multiline_alsoreq(void)
-{
-    test_config(existing_multiline_alsoreq_orig,
-                existing_multiline_alsoreq_expected,
-                AF_INET,
-                NULL,
-                0,
-                FALSE,
-                NM_DHCP_HOSTNAME_FLAG_NONE,
-                NULL,
-                NULL,
-                "eth0",
-                NULL,
-                NULL);
-}
-
-/*****************************************************************************/
-
-static void
-test_one_duid(const char *escaped, const guint8 *unescaped, guint len)
-{
-    gs_unref_bytes GBytes *t1 = NULL;
-    gs_unref_bytes GBytes *t2 = NULL;
-    gs_free char          *w  = NULL;
-
-    t1 = nm_dhcp_dhclient_unescape_duid(escaped);
-    g_assert(t1);
-    g_assert(nm_g_bytes_equal_mem(t1, unescaped, len));
-
-    t2 = g_bytes_new(unescaped, len);
-    w  = nm_dhcp_dhclient_escape_duid(t2);
-    g_assert(w);
-    g_assert_cmpstr(escaped, ==, w);
-}
-
-static void
-test_duids(void)
-{
-    const guint8 test1_u[] =
-        {0x00, 0x01, 0x00, 0x01, 0x13, 0x6f, 0x13, 0x6e, 0x00, 0x22, 0xfa, 0x8c, 0xd6, 0xc2};
-    const char *test1_s = "\\000\\001\\000\\001\\023o\\023n\\000\\\"\\372\\214\\326\\302";
-
-    const guint8 test2_u[] =
-        {0x00, 0x01, 0x00, 0x01, 0x17, 0x57, 0xee, 0x39, 0x00, 0x23, 0x15, 0x08, 0x7E, 0xac};
-    const char *test2_s = "\\000\\001\\000\\001\\027W\\3569\\000#\\025\\010~\\254";
-
-    const guint8 test3_u[] =
-        {0x00, 0x01, 0x00, 0x01, 0x17, 0x58, 0xe8, 0x58, 0x00, 0x23, 0x15, 0x08, 0x7e, 0xac};
-    const char *test3_s = "\\000\\001\\000\\001\\027X\\350X\\000#\\025\\010~\\254";
-
-    const guint8 test4_u[] =
-        {0x00, 0x01, 0x00, 0x01, 0x15, 0xd5, 0x31, 0x97, 0x00, 0x16, 0xeb, 0x04, 0x45, 0x18};
-    const char *test4_s = "\\000\\001\\000\\001\\025\\3251\\227\\000\\026\\353\\004E\\030";
-
-    const char *bad_s = "\\000\\001\\000\\001\\425\\3251\\227\\000\\026\\353\\004E\\030";
-
-    test_one_duid(test1_s, test1_u, sizeof(test1_u));
-    test_one_duid(test2_s, test2_u, sizeof(test2_u));
-    test_one_duid(test3_s, test3_u, sizeof(test3_u));
-    test_one_duid(test4_s, test4_u, sizeof(test4_u));
-
-    /* Invalid octal digit */
-    g_assert(nm_dhcp_dhclient_unescape_duid(bad_s) == NULL);
-}
-
-static void
-test_read_duid_from_leasefile(void)
-{
-    const guint8 expected[] =
-        {0x00, 0x01, 0x00, 0x01, 0x18, 0x79, 0xa6, 0x13, 0x60, 0x67, 0x20, 0xec, 0x4c, 0x70};
-    gs_unref_bytes GBytes *duid  = NULL;
-    GError                *error = NULL;
-
-    duid = nm_dhcp_dhclient_read_duid(TEST_DIR "/test-dhclient-duid.leases", &error);
-    nmtst_assert_success(duid, error);
-
-    g_assert(nm_g_bytes_equal_mem(duid, expected, G_N_ELEMENTS(expected)));
-}
-
-static void
-test_read_commented_duid_from_leasefile(void)
-{
-    GBytes *duid;
-    GError *error = NULL;
-
-    duid = nm_dhcp_dhclient_read_duid(TEST_DIR "/test-dhclient-commented-duid.leases", &error);
-    g_assert_no_error(error);
-    g_assert(duid == NULL);
-}
-
-/*****************************************************************************/
-
-static void
-_check_duid_impl(const guint8 *duid_bin,
-                 gsize         duid_len,
-                 gboolean      enforce_duid,
-                 const char   *old_content,
-                 const char   *new_content)
-{
-    gs_free_error GError  *error    = NULL;
-    gs_free char          *contents = NULL;
-    gboolean               success;
-    const char            *path = NM_BUILD_BUILDDIR "/src/core/dhcp/tests/check-duid.lease";
-    gs_unref_bytes GBytes *duid = NULL;
-    gsize                  contents_len;
-
-    g_assert(duid_bin);
-    g_assert(duid_len > 0);
-
-    if (!nm_str_is_empty(old_content) || nmtst_get_rand_bool()) {
-        success = g_file_set_contents(path, old_content ?: "", -1, &error);
-        nmtst_assert_success(success, error);
-    } else
-        nmtst_file_unlink_if_exists(path);
-
-    duid = g_bytes_new(duid_bin, duid_len);
-
-    success = nm_dhcp_dhclient_save_duid(path, duid, enforce_duid, &error);
-    nmtst_assert_success(success, error);
-
-    success = g_file_get_contents(path, &contents, &contents_len, &error);
-    nmtst_assert_success(success, error);
-    g_assert(contents);
-
-    nmtst_file_unlink(path);
-
-    if (!nm_streq0(new_content, contents))
-        g_error("FAILING:\n\nEXPECTED:\n%s\nACTUAL:\n%s\n\n", new_content, contents);
-
-    g_assert_cmpstr(new_content, ==, contents);
-    g_assert_cmpint(contents_len, ==, strlen(contents));
-}
-
-#define _DUID(...) ((const guint8[]) {__VA_ARGS__})
-
-#define _check_duid(duid, enforce_duid, old_content, new_content) \
-    _check_duid_impl((duid), sizeof(duid), (enforce_duid), (old_content), (new_content))
-
-static void
-test_write_duid(void)
-{
-    _check_duid(_DUID(000, 001, 000, 001, 027, 'X', 0350, 'X', 0, '#', 025, 010, '~', 0254),
-                FALSE,
-                NULL,
-                "default-duid \"\\000\\001\\000\\001\\027X\\350X\\000#\\025\\010~\\254\";\n");
-
-    _check_duid(
-        _DUID(000, 001, 000, 001, 023, 'o', 023, 'n', 000, '"', 0372, 0214, 0326, 0302),
-        FALSE,
-        "default-duid \"\\000\\001\\000\\001\\027X\\350X\\000#\\025\\010~\\254\";\n",
-        "default-duid \"\\000\\001\\000\\001\\023o\\023n\\000\\\"\\372\\214\\326\\302\";\n");
-
-    _check_duid(_DUID(000, 001, 000, 001, 023, 'o', 023, 'n', 000, '"', 0372, 0214, 0326, 0302),
-                FALSE,
-                "#default-duid \"\\000\\001\\000\\001\\027X\\350X\\000#\\025\\010~\\254\";\n",
-                "default-duid "
-                "\"\\000\\001\\000\\001\\023o\\023n\\000\\\"\\372\\214\\326\\302\";\n#default-duid "
-                "\"\\000\\001\\000\\001\\027X\\350X\\000#\\025\\010~\\254\";\n");
-    _check_duid(
-        _DUID(000, 001, 000, 001, 023, 'o', 023, 'n', 000, '"', 0372, 0214, 0326, 0302),
-        FALSE,
-        "### Commented old DUID ###\n#default-duid "
-        "\"\\000\\001\\000\\001\\027X\\350X\\000#\\025\\010~\\254\";\n",
-        "default-duid \"\\000\\001\\000\\001\\023o\\023n\\000\\\"\\372\\214\\326\\302\";\n### "
-        "Commented old DUID ###\n#default-duid "
-        "\"\\000\\001\\000\\001\\027X\\350X\\000#\\025\\010~\\254\";\n");
-
-    _check_duid(
-        _DUID(0xaa, 0xb, 0xcc, 0xd, 0xee, 0xf),
-        FALSE,
-        "default-duid \"\\252\\013\\314\\015\\356\\017\";\nlease6 {\n  interface \"eth1\";\n "
-        " ia-na f1:ce:00:01 {\n    starts 1671015678;\n    renew 60;\n    rebind 105;\n    "
-        "iaaddr 192:168:121::1:112c {\n      starts 1671015678;\n      preferred-life 120;\n "
-        "     max-life 120;\n    }\n  }\n  option fqdn.encoded true;\n  option "
-        "fqdn.server-update true;\n  option fqdn.no-client-update false;\n  option fqdn.fqdn "
-        "\"dff6de4fcb0f\";\n  option fqdn.hostname \"dff6de4fcb0f\";\n  option dhcp6.client-id "
-        "aa:b:cc:d:ee:f;\n  option dhcp6.server-id 0:1:0:1:2b:2c:4d:1d:0:0:0:0:0:0;\n  option "
-        "dhcp6.name-servers 192:168:121:0:ce0f:f1ff:fece:1;\n  option dhcp6.fqdn "
-        "1:c:64:66:66:36:64:65:34:66:63:62:30:66;\n  option dhcp6.status-code success "
-        "\"success\";\n}\n",
-        "default-duid \"\\252\\013\\314\\015\\356\\017\";\nlease6 {\n  interface \"eth1\";\n "
-        " ia-na f1:ce:00:01 {\n    starts 1671015678;\n    renew 60;\n    rebind 105;\n    "
-        "iaaddr 192:168:121::1:112c {\n      starts 1671015678;\n      preferred-life 120;\n "
-        "     max-life 120;\n    }\n  }\n  option fqdn.encoded true;\n  option "
-        "fqdn.server-update true;\n  option fqdn.no-client-update false;\n  option fqdn.fqdn "
-        "\"dff6de4fcb0f\";\n  option fqdn.hostname \"dff6de4fcb0f\";\n  option dhcp6.client-id "
-        "aa:b:cc:d:ee:f;\n  option dhcp6.server-id 0:1:0:1:2b:2c:4d:1d:0:0:0:0:0:0;\n  option "
-        "dhcp6.name-servers 192:168:121:0:ce0f:f1ff:fece:1;\n  option dhcp6.fqdn "
-        "1:c:64:66:66:36:64:65:34:66:63:62:30:66;\n  option dhcp6.status-code success "
-        "\"success\";\n}\n");
-
-    _check_duid(
-        _DUID(0xaa, 0xb, 0xcc, 0xd, 0xee, 0xf),
-        FALSE,
-        "default-duid \"\\252\\013\\314\\015\\356\\017\";\nlease6 {\n  interface \"eth1\";\n "
-        " ia-na f1:ce:00:01 {\n    starts 1671015678;\n    renew 60;\n    rebind 105;\n    "
-        "iaaddr 192:168:121::1:112c {\n      starts 1671015678;\n      preferred-life 120;\n "
-        "     max-life 120;\n    }\n  }\n  option fqdn.encoded true;\n  option "
-        "fqdn.server-update true;\n  option fqdn.no-client-update false;\n  option fqdn.fqdn "
-        "\"dff6de4fcb0f\";\n  option fqdn.hostname \"dff6de4fcb0f\";\n  option dhcp6.client-id "
-        "aa:b:cc:d:ee:f;\n  option dhcp6.server-id 0:1:0:1:2b:2c:4d:1d:0:0:0:0:0:0;\n  option "
-        "dhcp6.name-servers 192:168:121:0:ce0f:f1ff:fece:1;\n  option dhcp6.fqdn "
-        "1:c:64:66:66:36:64:65:34:66:63:62:30:66;\n  option dhcp6.status-code success "
-        "\"success\";\r\n}\n",
-        "default-duid \"\\252\\013\\314\\015\\356\\017\";\nlease6 {\n  interface \"eth1\";\n "
-        " ia-na f1:ce:00:01 {\n    starts 1671015678;\n    renew 60;\n    rebind 105;\n    "
-        "iaaddr 192:168:121::1:112c {\n      starts 1671015678;\n      preferred-life 120;\n "
-        "     max-life 120;\n    }\n  }\n  option fqdn.encoded true;\n  option "
-        "fqdn.server-update true;\n  option fqdn.no-client-update false;\n  option fqdn.fqdn "
-        "\"dff6de4fcb0f\";\n  option fqdn.hostname \"dff6de4fcb0f\";\n  option dhcp6.client-id "
-        "aa:b:cc:d:ee:f;\n  option dhcp6.server-id 0:1:0:1:2b:2c:4d:1d:0:0:0:0:0:0;\n  option "
-        "dhcp6.name-servers 192:168:121:0:ce0f:f1ff:fece:1;\n  option dhcp6.fqdn "
-        "1:c:64:66:66:36:64:65:34:66:63:62:30:66;\n  option dhcp6.status-code success "
-        "\"success\";\r\n}\n");
-
-    _check_duid(
-        _DUID(0xaa, 0xb, 0xcc, 0xd, 0xee, 0xe),
-        FALSE,
-        "default-duid \"\\252\\013\\314\\015\\356\\017\";\nlease6 {\n  interface \"eth1\";\n "
-        " ia-na f1:ce:00:01 {\n    starts 1671015678;\n    renew 60;\n    rebind 105;\n    "
-        "iaaddr 192:168:121::1:112c {\n      starts 1671015678;\n      preferred-life 120;\n "
-        "     max-life 120;\n    }\n  }\n  option fqdn.encoded true;\n  option "
-        "fqdn.server-update true;\n  option fqdn.no-client-update false;\n  option fqdn.fqdn "
-        "\"dff6de4fcb0f\";\n  option fqdn.hostname \"dff6de4fcb0f\";\n  option dhcp6.client-id "
-        "aa:b:cc:d:ee:f;\n  option dhcp6.server-id 0:1:0:1:2b:2c:4d:1d:0:0:0:0:0:0;\n  option "
-        "dhcp6.name-servers 192:168:121:0:ce0f:f1ff:fece:1;\n  option dhcp6.fqdn "
-        "1:c:64:66:66:36:64:65:34:66:63:62:30:66;\n  option dhcp6.status-code success "
-        "\"success\";\r\n}\n",
-        "default-duid \"\\252\\013\\314\\015\\356\\016\";\nlease6 {\n  interface \"eth1\";\n "
-        " ia-na f1:ce:00:01 {\n    starts 1671015678;\n    renew 60;\n    rebind 105;\n    "
-        "iaaddr 192:168:121::1:112c {\n      starts 1671015678;\n      preferred-life 120;\n "
-        "     max-life 120;\n    }\n  }\n  option fqdn.encoded true;\n  option "
-        "fqdn.server-update true;\n  option fqdn.no-client-update false;\n  option fqdn.fqdn "
-        "\"dff6de4fcb0f\";\n  option fqdn.hostname \"dff6de4fcb0f\";\n  option dhcp6.client-id "
-        "aa:b:cc:d:ee:f;\n  option dhcp6.server-id 0:1:0:1:2b:2c:4d:1d:0:0:0:0:0:0;\n  option "
-        "dhcp6.name-servers 192:168:121:0:ce0f:f1ff:fece:1;\n  option dhcp6.fqdn "
-        "1:c:64:66:66:36:64:65:34:66:63:62:30:66;\n  option dhcp6.status-code success "
-        "\"success\";\r\n}\n");
-
-    _check_duid(
-        _DUID(0xaa, 0xb, 0xcc, 0xd, 0xee, 0xe),
-        TRUE,
-        "default-duid \"\\252\\013\\314\\015\\356\\017\";\nlease6 {\n  interface \"eth1\";\n "
-        " ia-na f1:ce:00:01 {\n    starts 1671015678;\n    renew 60;\n    rebind 105;\n    "
-        "iaaddr 192:168:121::1:112c {\n      starts 1671015678;\n      preferred-life 120;\n "
-        "     max-life 120;\n    }\n  }\n  option fqdn.encoded true;\n  option "
-        "fqdn.server-update true;\n  option fqdn.no-client-update false;\n  option fqdn.fqdn "
-        "\"dff6de4fcb0f\";\n  option fqdn.hostname \"dff6de4fcb0f\";\n  option dhcp6.client-id "
-        "aa:b:cc:d:ee:f;\n  option dhcp6.server-id 0:1:0:1:2b:2c:4d:1d:0:0:0:0:0:0;\n  option "
-        "dhcp6.name-servers 192:168:121:0:ce0f:f1ff:fece:1;\n  option dhcp6.fqdn "
-        "1:c:64:66:66:36:64:65:34:66:63:62:30:66;\n  option dhcp6.status-code success "
-        "\"success\";\n}\n",
-        "default-duid \"\\252\\013\\314\\015\\356\\016\";\nlease6 {\n  interface \"eth1\";\n "
-        " ia-na f1:ce:00:01 {\n    starts 1671015678;\n    renew 60;\n    rebind 105;\n    "
-        "iaaddr 192:168:121::1:112c {\n      starts 1671015678;\n      preferred-life 120;\n "
-        "     max-life 120;\n    }\n  }\n  option fqdn.encoded true;\n  option "
-        "fqdn.server-update true;\n  option fqdn.no-client-update false;\n  option fqdn.fqdn "
-        "\"dff6de4fcb0f\";\n  option fqdn.hostname \"dff6de4fcb0f\";\n  option dhcp6.client-id "
-        "aa:b:cc:d:ee:e;\n  option dhcp6.server-id 0:1:0:1:2b:2c:4d:1d:0:0:0:0:0:0;\n  option "
-        "dhcp6.name-servers 192:168:121:0:ce0f:f1ff:fece:1;\n  option dhcp6.fqdn "
-        "1:c:64:66:66:36:64:65:34:66:63:62:30:66;\n  option dhcp6.status-code success "
-        "\"success\";\n}\n");
-}
-
-/*****************************************************************************/
-
-static const char *interface1_orig = "interface \"eth0\" {\n"
-                                     "\talso request my-option;\n"
-                                     "\tinitial-delay 5;\n"
-                                     "}\n"
-                                     "interface \"eth1\" {\n"
-                                     "\talso request another-option;\n"
-                                     "\tinitial-delay 0;\n"
-                                     "}\n"
-                                     "\n"
-                                     "also request yet-another-option;\n";
-
-static const char *interface1_expected =
-    "# Created by NetworkManager\n"
-    "# Merged from /path/to/dhclient.conf\n"
-    "\n"
-    "initial-delay 5;\n"
-    "\n"
-    "option rfc3442-classless-static-routes code 121 = array of unsigned integer 8;\n"
-    "option ms-classless-static-routes code 249 = array of unsigned integer 8;\n"
-    "option wpad code 252 = string;\n"
-    "\n"
-    "also request my-option;\n"
-    "also request yet-another-option;\n"
-    "also request rfc3442-classless-static-routes;\n"
-    "also request ms-classless-static-routes;\n"
-    "also request static-routes;\n"
-    "also request wpad;\n"
-    "also request ntp-servers;\n"
-    "also request root-path;\n"
-    "\n";
-
-static void
-test_interface1(void)
-{
-    test_config(interface1_orig,
-                interface1_expected,
-                AF_INET,
-                NULL,
-                0,
-                FALSE,
-                NM_DHCP_HOSTNAME_FLAG_NONE,
-                NULL,
-                NULL,
-                "eth0",
-                NULL,
-                NULL);
-}
-
-/*****************************************************************************/
-
-static const char *interface2_orig = "interface eth0 {\n"
-                                     "\talso request my-option;\n"
-                                     "\tinitial-delay 5;\n"
-                                     " }\n"
-                                     "interface eth1 {\n"
-                                     "\tinitial-delay 0;\n"
-                                     "\trequest another-option;\n"
-                                     " } \n"
-                                     "\n"
-                                     "also request yet-another-option;\n";
-
-static const char *interface2_expected =
-    "# Created by NetworkManager\n"
-    "# Merged from /path/to/dhclient.conf\n"
-    "\n"
-    "initial-delay 0;\n"
-    "\n"
-    "option rfc3442-classless-static-routes code 121 = array of unsigned integer 8;\n"
-    "option ms-classless-static-routes code 249 = array of unsigned integer 8;\n"
-    "option wpad code 252 = string;\n"
-    "\n"
-    "request; # override dhclient defaults\n"
-    "also request another-option;\n"
-    "also request yet-another-option;\n"
-    "also request rfc3442-classless-static-routes;\n"
-    "also request ms-classless-static-routes;\n"
-    "also request static-routes;\n"
-    "also request wpad;\n"
-    "also request ntp-servers;\n"
-    "also request root-path;\n"
-    "\n";
-
-static void
-test_interface2(void)
-{
-    test_config(interface2_orig,
-                interface2_expected,
-                AF_INET,
-                NULL,
-                0,
-                FALSE,
-                NM_DHCP_HOSTNAME_FLAG_NONE,
-                NULL,
-                NULL,
-                "eth1",
-                NULL,
-                NULL);
-}
-
-static void
-test_structured(void)
-{
-    gs_unref_bytes GBytes *new_client_id = NULL;
-    const guint8           bytes[]       = "sad-and-useless";
-
-    static const char *const orig =
-        "interface \"eth0\"   {  \n"
-        "    send host-name \"useless.example.com\";\n"
-        "    hardware ethernet de:ad:80:86:ba:be;\n"
-        "    send dhcp-client-identifier \"sad-and-useless\";\n"
-        "    script \"/bin/useless\";\n"
-        "    send dhcp-lease-time 8086;\n"
-        "    request subnet-mask, broadcast-address, time-offset, routers,\n"
-        "        domain-search, domain-name, host-name;\n"
-        "    require subnet-mask;\n"
-        "}  \n"
-        "\n"
-        "    interface \"eth1\"   {  \n"
-        "    send host-name \"sad.example.com\";\n"
-        "    hardware ethernet de:ca:f6:66:ca:fe;\n"
-        "    send dhcp-client-identifier \"useless-and-miserable\";\n"
-        "    script \"/bin/miserable\";\n"
-        "    send dhcp-lease-time 1337;\n"
-        "    request subnet-mask, broadcast-address, time-offset, routers,\n"
-        "        domain-search, domain-name, domain-name-servers, host-name;\n"
-        "    require subnet-mask, domain-name-servers;\n"
-        "    if not option domain-name = \"example.org\" {\n"
-        "        prepend domain-name-servers 127.0.0.1;\n"
-        "    } else {\n"
-        "        prepend domain-name-servers 127.0.0.2;\n"
-        "    }  \n"
-        "    }  \n"
-        "\n"
-        "pseudo \"secondary\" \"eth0\"   {  \n"
-        "    send dhcp-client-identifier \"sad-useless-and-secondary\";\n"
-        "    script \"/bin/secondary\";\n"
-        "    send host-name \"secondary.useless.example.com\";\n"
-        "    send dhcp-lease-time 666;\n"
-        "    request routers;\n"
-        "    require routers;\n"
-        "    }  \n"
-        "\n"
-        "    pseudo \"tertiary\" \"eth0\"   {  \n"
-        "   send dhcp-client-identifier \"sad-useless-and-tertiary\";\n"
-        "  script \"/bin/tertiary\";\n"
-        " send host-name \"tertiary.useless.example.com\";\n"
-        "}  \n"
-        "\n"
-        "  alias{  \n"
-        "    interface \"eth0\";\n"
-        "    fixed-address 192.0.2.1;\n"
-        "    option subnet-mask 255.255.255.0;\n"
-        "  }  \n"
-        "  lease   {  \n"
-        "    interface \"eth0\";\n"
-        "    fixed-address 192.0.2.2;\n"
-        "    option subnet-mask 255.255.255.0;\n"
-        "  }  \n"
-        "if not option domain-name = \"example.org\" {\n"
-        "  prepend domain-name-servers 127.0.0.1;\n"
-        "  if not option domain-name = \"useless.example.com\" {\n"
-        "    prepend domain-name-servers 127.0.0.2;\n"
-        "  }\n"
-        "}\n";
-
-    static const char *const expected =
-        "# Created by NetworkManager\n"
-        "# Merged from /path/to/dhclient.conf\n"
-        "\n"
-        "send host-name \"useless.example.com\";\n"
-        "hardware ethernet de:ad:80:86:ba:be;\n"
-        "send dhcp-client-identifier \"sad-and-useless\";\n"
-        "send dhcp-lease-time 8086;\n"
-        "require subnet-mask;\n"
-        "if not option domain-name = \"example.org\" {\n"
-        "prepend domain-name-servers 127.0.0.1;\n"
-        "if not option domain-name = \"useless.example.com\" {\n"
-        "prepend domain-name-servers 127.0.0.2;\n"
-        "}\n"
-        "}\n"
-        "\n"
-        "option rfc3442-classless-static-routes code 121 = array of unsigned integer 8;\n"
-        "option ms-classless-static-routes code 249 = array of unsigned integer 8;\n"
-        "option wpad code 252 = string;\n"
-        "\n"
-        "request; # override dhclient defaults\n"
-        "also request subnet-mask;\n"
-        "also request broadcast-address;\n"
-        "also request time-offset;\n"
-        "also request routers;\n"
-        "also request domain-search;\n"
-        "also request domain-name;\n"
-        "also request host-name;\n"
-        "also request rfc3442-classless-static-routes;\n"
-        "also request ms-classless-static-routes;\n"
-        "also request static-routes;\n"
-        "also request wpad;\n"
-        "also request ntp-servers;\n"
-        "also request root-path;\n"
-        "\n";
-
-    new_client_id = g_bytes_new(bytes, sizeof(bytes) - 1);
-    test_config(orig,
-                expected,
-                AF_INET,
-                NULL,
-                0,
-                FALSE,
-                NM_DHCP_HOSTNAME_FLAG_NONE,
-                NULL,
-                new_client_id,
-                "eth0",
-                NULL,
-                NULL);
-}
-
-static void
-test_config_req_intf(void)
-{
-    static const char *const orig = "request subnet-mask, broadcast-address, routers,\n"
-                                    "\trfc3442-classless-static-routes,\n"
-                                    "\tinterface-mtu, host-name, domain-name, domain-search,\n"
-                                    "\tdomain-name-servers, nis-domain, nis-servers,\n"
-                                    "\tnds-context, nds-servers, nds-tree-name,\n"
-                                    "\tnetbios-name-servers, netbios-dd-server,\n"
-                                    "\tnetbios-node-type, netbios-scope, ntp-servers;\n"
-                                    "";
-    static const char *const expected =
-        "# Created by NetworkManager\n"
-        "# Merged from /path/to/dhclient.conf\n"
-        "\n"
-        "\n"
-        "option rfc3442-classless-static-routes code 121 = array of unsigned integer 8;\n"
-        "option ms-classless-static-routes code 249 = array of unsigned integer 8;\n"
-        "option wpad code 252 = string;\n"
-        "\n"
-        "request; # override dhclient defaults\n"
-        "also request subnet-mask;\n"
-        "also request broadcast-address;\n"
-        "also request routers;\n"
-        "also request rfc3442-classless-static-routes;\n"
-        "also request interface-mtu;\n"
-        "also request host-name;\n"
-        "also request domain-name;\n"
-        "also request domain-search;\n"
-        "also request domain-name-servers;\n"
-        "also request nis-domain;\n"
-        "also request nis-servers;\n"
-        "also request nds-context;\n"
-        "also request nds-servers;\n"
-        "also request nds-tree-name;\n"
-        "also request netbios-name-servers;\n"
-        "also request netbios-dd-server;\n"
-        "also request netbios-node-type;\n"
-        "also request netbios-scope;\n"
-        "also request ntp-servers;\n"
-        "also request ms-classless-static-routes;\n"
-        "also request static-routes;\n"
-        "also request wpad;\n"
-        "also request root-path;\n"
-        "\n";
-
-    test_config(orig,
-                expected,
-                AF_INET,
-                NULL,
-                0,
-                FALSE,
-                NM_DHCP_HOSTNAME_FLAG_NONE,
-                NULL,
-                NULL,
-                "eth0",
-                NULL,
-                NULL);
-}
-
-/*****************************************************************************/
-
-NMTST_DEFINE();
-
-int
-main(int argc, char **argv)
-{
-    nmtst_init_with_logging(&argc, &argv, NULL, "DEFAULT");
-
-    g_test_add_func("/dhcp/dhclient/orig_missing", test_orig_missing);
-    g_test_add_func("/dhcp/dhclient/orig_missing_add_mud_url", test_orig_missing_add_mud_url);
-    g_test_add_func("/dhcp/dhclient/override_client_id", test_override_client_id);
-    g_test_add_func("/dhcp/dhclient/quote_client_id/1", test_quote_client_id);
-    g_test_add_func("/dhcp/dhclient/quote_client_id/2", test_quote_client_id_2);
-    g_test_add_func("/dhcp/dhclient/hex_zero_client_id", test_hex_zero_client_id);
-    g_test_add_func("/dhcp/dhclient/ascii_client_id", test_ascii_client_id);
-    g_test_add_func("/dhcp/dhclient/hex_single_client_id", test_hex_single_client_id);
-    g_test_add_func("/dhcp/dhclient/existing-hex-client-id", test_existing_hex_client_id);
-    g_test_add_func("/dhcp/dhclient/existing-client-id", test_existing_escaped_client_id);
-    g_test_add_func("/dhcp/dhclient/existing-ascii-client-id", test_existing_ascii_client_id);
-    g_test_add_func("/dhcp/dhclient/none-client-id", test_none_client_id);
-    g_test_add_func("/dhcp/dhclient/missing-client-id", test_missing_client_id);
-    g_test_add_func("/dhcp/dhclient/fqdn", test_fqdn);
-    g_test_add_func("/dhcp/dhclient/fqdn_options_override", test_fqdn_options_override);
-    g_test_add_func("/dhcp/dhclient/override_hostname", test_override_hostname);
-    g_test_add_func("/dhcp/dhclient/override_hostname6", test_override_hostname6);
-    g_test_add_func("/dhcp/dhclient/nonfqdn_hostname6", test_nonfqdn_hostname6);
-    g_test_add_func("/dhcp/dhclient/existing_req", test_existing_req);
-    g_test_add_func("/dhcp/dhclient/existing_alsoreq", test_existing_alsoreq);
-    g_test_add_func("/dhcp/dhclient/existing_multiline_alsoreq", test_existing_multiline_alsoreq);
-    g_test_add_func("/dhcp/dhclient/duids", test_duids);
-    g_test_add_func("/dhcp/dhclient/interface/1", test_interface1);
-    g_test_add_func("/dhcp/dhclient/interface/2", test_interface2);
-    g_test_add_func("/dhcp/dhclient/config/req_intf", test_config_req_intf);
-    g_test_add_func("/dhcp/dhclient/structured", test_structured);
-
-    g_test_add_func("/dhcp/dhclient/read_duid_from_leasefile", test_read_duid_from_leasefile);
-    g_test_add_func("/dhcp/dhclient/read_commented_duid_from_leasefile",
-                    test_read_commented_duid_from_leasefile);
-
-    g_test_add_func("/dhcp/dhclient/test_write_duid", test_write_duid);
-
-    return g_test_run();
-}
diff --git a/src/core/dns/nm-dns-manager.c b/src/core/dns/nm-dns-manager.c
index ec33c464..f1a19d01 100644
--- a/src/core/dns/nm-dns-manager.c
+++ b/src/core/dns/nm-dns-manager.c
@@ -371,7 +371,7 @@ _ASSERT_dns_config_ip_data(const NMDnsConfigIPData *ip_data)
         gboolean has_default = FALSE;
         gsize    i;
 
-        for (i = 0; ip_data->domains.search && ip_data->domains.search; i++) {
+        for (i = 0; ip_data->domains.search && ip_data->domains.search[i]; i++) {
             const char *d = ip_data->domains.search[i];
 
             d = nm_utils_parse_dns_domain(d, NULL);
diff --git a/src/core/main.c b/src/core/main.c
index 8d519c00..7299a91c 100644
--- a/src/core/main.c
+++ b/src/core/main.c
@@ -298,12 +298,6 @@ main(int argc, char *argv[])
 
     _nm_utils_is_manager_process = TRUE;
 
-    /* Known to cause a possible deadlock upon GDBus initialization:
-     * https://bugzilla.gnome.org/show_bug.cgi?id=674885 */
-    g_type_ensure(G_TYPE_SOCKET);
-    g_type_ensure(G_TYPE_DBUS_CONNECTION);
-    g_type_ensure(NM_TYPE_DBUS_MANAGER);
-
     /* we determine a first-start (contrary to a restart during the same boot)
      * based on the existence of NM_CONFIG_DEVICE_STATE_DIR directory. */
     config_cli = nm_config_cmd_line_options_new(
@@ -328,6 +322,12 @@ main(int argc, char *argv[])
         exit(result);
     }
 
+    /* Known to cause a possible deadlock upon GDBus initialization:
+     * https://bugzilla.gnome.org/show_bug.cgi?id=674885 */
+    g_type_ensure(G_TYPE_SOCKET);
+    g_type_ensure(G_TYPE_DBUS_CONNECTION);
+    g_type_ensure(NM_TYPE_DBUS_MANAGER);
+
     nm_main_utils_ensure_not_running_pidfile(global_opt.pidfile);
 
     nm_main_utils_ensure_statedir();
@@ -461,14 +461,8 @@ main(int argc, char *argv[])
     /* the first access to State causes the file to be read (and possibly print a warning) */
     nm_config_state_get(config);
 
-    nm_log_dbg(LOGD_CORE,
-               "WEXT support is %s",
-#if HAVE_WEXT
-               "enabled"
-#else
-               "disabled"
-#endif
-    );
+    nm_log_dbg(LOGD_CORE, "WEXT support is %s", HAVE_WEXT ? "enabled" : "disabled");
+    nm_log_dbg(LOGD_CORE, "CLAT support is %s", HAVE_CLAT ? "enabled" : "disabled");
 
     if (!_dbus_manager_init(config))
         goto done_no_manager;
diff --git a/src/core/meson.build b/src/core/meson.build
index b1d7f2e7..8ff3d9f9 100644
--- a/src/core/meson.build
+++ b/src/core/meson.build
@@ -32,6 +32,15 @@ install_data(
 
 core_plugins = []
 
+subdir('bpf')
+
+base_sources_addon = []
+base_deps_addon = []
+if enable_clat
+  base_sources_addon += [clat_skel_h]
+  base_deps_addon += [libbpf]
+endif
+
 libNetworkManagerBase = static_library(
   'NetworkManagerBase',
   sources: files(
@@ -55,20 +64,20 @@ libNetworkManagerBase = static_library(
     'nm-l3cfg.c',
     'nm-bond-manager.c',
     'nm-ip-config.c',
-  ),
+  ) + base_sources_addon,
   dependencies: [
     core_default_dep,
     libnm_core_public_dep,
+    libndp_dep,
     libsystemd_dep,
     libudev_dep,
-  ],
+  ] + base_deps_addon,
 )
 
 nm_deps = [
   libnm_core_public_dep,
   core_default_dep,
   dl_dep,
-  libndp_dep,
   libudev_dep,
   logind_dep,
 ]
@@ -119,8 +128,6 @@ libNetworkManager = static_library(
     'devices/nm-device-wireguard.c',
     'devices/nm-device-wpan.c',
     'devices/nm-lldp-listener.c',
-    'dhcp/nm-dhcp-dhclient.c',
-    'dhcp/nm-dhcp-dhclient-utils.c',
     'dhcp/nm-dhcp-dhcpcd.c',
     'dhcp/nm-dhcp-listener.c',
     'dns/nm-dns-dnsmasq.c',
diff --git a/src/core/ndisc/nm-lndp-ndisc.c b/src/core/ndisc/nm-lndp-ndisc.c
index c19dcc91..69213da6 100644
--- a/src/core/ndisc/nm-lndp-ndisc.c
+++ b/src/core/ndisc/nm-lndp-ndisc.c
@@ -401,6 +401,30 @@ receive_ra(struct ndp *ndp, struct ndp_msg *msg, gpointer user_data)
         }
     }
 
+#if HAVE_CLAT
+    /* PREF64 */
+    ndp_msg_opt_for_each_offset (offset, msg, NDP_MSG_OPT_PREF64) {
+        NMNDiscPref64 pref64;
+
+        pref64 = (NMNDiscPref64) {
+            .prefix             = *ndp_msg_opt_pref64_prefix(msg, offset),
+            .plen               = ndp_msg_opt_pref64_prefix_length(msg, offset),
+            .gateway            = gateway.address,
+            .gateway_preference = gateway.preference,
+            .expiry_msec =
+                _nm_ndisc_lifetime_to_expiry(now_msec, ndp_msg_opt_pref64_lifetime(msg, offset)),
+            .gateway_expiry_msec = gateway.expiry_msec,
+        };
+
+        /* libndp should only return lengths defined in RFC 8781 */
+        nm_assert(NM_IN_SET(pref64.plen, 96, 64, 56, 48, 40, 32));
+
+        if (nm_ndisc_add_pref64(ndisc, &pref64, now_msec)) {
+            changed |= NM_NDISC_CONFIG_PREF64;
+        }
+    }
+#endif
+
     nm_ndisc_ra_received(ndisc, now_msec, changed);
     return 0;
 }
@@ -463,7 +487,8 @@ send_ra(NMNDisc *ndisc, GError **error)
     struct in6_addr         *addr;
     struct ndp_msg          *msg;
     guint                    i;
-    nm_auto_str_buf NMStrBuf sbuf = NM_STR_BUF_INIT(0, FALSE);
+    nm_auto_str_buf NMStrBuf sbuf     = NM_STR_BUF_INIT(0, FALSE);
+    gint64                   now_msec = nm_utils_get_monotonic_timestamp_msec();
 
     errsv = ndp_msg_new(&msg, NDP_MSG_RA);
     if (errsv) {
@@ -507,13 +532,9 @@ send_ra(NMNDisc *ndisc, GError **error)
         prefix->nd_opt_pi_flags_reserved |= ND_OPT_PI_FLAG_ONLINK;
         prefix->nd_opt_pi_flags_reserved |= ND_OPT_PI_FLAG_AUTO;
         prefix->nd_opt_pi_valid_time =
-            htonl(_nm_ndisc_lifetime_from_expiry(NM_NDISC_EXPIRY_BASE_TIMESTAMP,
-                                                 address->expiry_msec,
-                                                 TRUE));
+            htonl(_nm_ndisc_lifetime_from_expiry(now_msec, address->expiry_msec, TRUE));
         prefix->nd_opt_pi_preferred_time =
-            htonl(_nm_ndisc_lifetime_from_expiry(NM_NDISC_EXPIRY_BASE_TIMESTAMP,
-                                                 address->expiry_preferred_msec,
-                                                 TRUE));
+            htonl(_nm_ndisc_lifetime_from_expiry(now_msec, address->expiry_preferred_msec, TRUE));
         prefix->nd_opt_pi_prefix.s6_addr32[0] = address->address.s6_addr32[0];
         prefix->nd_opt_pi_prefix.s6_addr32[1] = address->address.s6_addr32[1];
         prefix->nd_opt_pi_prefix.s6_addr32[2] = 0;
diff --git a/src/core/ndisc/nm-ndisc-private.h b/src/core/ndisc/nm-ndisc-private.h
index 1479e566..90ec032d 100644
--- a/src/core/ndisc/nm-ndisc-private.h
+++ b/src/core/ndisc/nm-ndisc-private.h
@@ -14,6 +14,7 @@ struct _NMNDiscDataInternal {
     NMNDiscData public;
     GArray *gateways;
     GArray *addresses;
+    GArray *pref64;
     GArray *routes;
     GArray *dns_servers;
     GArray *dns_domains;
@@ -28,6 +29,7 @@ gboolean nm_ndisc_add_gateway(NMNDisc *ndisc, const NMNDiscGateway *new_item, gi
 gboolean
 nm_ndisc_complete_and_add_address(NMNDisc *ndisc, const NMNDiscAddress *new_item, gint64 now_msec);
 gboolean nm_ndisc_add_route(NMNDisc *ndisc, const NMNDiscRoute *new_item, gint64 now_msec);
+gboolean nm_ndisc_add_pref64(NMNDisc *ndisc, const NMNDiscPref64 *new_item, gint64 now_msec);
 gboolean nm_ndisc_add_dns_server(NMNDisc *ndisc, const NMNDiscDNSServer *new_item, gint64 now_msec);
 gboolean nm_ndisc_add_dns_domain(NMNDisc *ndisc, const NMNDiscDNSDomain *new_item, gint64 now_msec);
 
diff --git a/src/core/ndisc/nm-ndisc.c b/src/core/ndisc/nm-ndisc.c
index 1a2bf480..2056b5f8 100644
--- a/src/core/ndisc/nm-ndisc.c
+++ b/src/core/ndisc/nm-ndisc.c
@@ -34,6 +34,7 @@
 #define _SIZE_MAX_ROUTES      1000u
 #define _SIZE_MAX_DNS_SERVERS 64u
 #define _SIZE_MAX_DNS_DOMAINS 64u
+#define _SIZE_MAX_PREF64      8u
 
 /*****************************************************************************/
 
@@ -109,7 +110,8 @@ nm_ndisc_data_to_l3cd(NMDedupMultiIndex        *multi_idx,
                       int                       ifindex,
                       const NMNDiscData        *rdata,
                       NMSettingIP6ConfigPrivacy ip6_privacy,
-                      NMUtilsIPv6IfaceId       *token)
+                      NMUtilsIPv6IfaceId       *token,
+                      const char               *network_id)
 {
     nm_auto_unref_l3cd_init NML3ConfigData *l3cd = NULL;
     guint32                                 ifa_flags;
@@ -211,13 +213,21 @@ nm_ndisc_data_to_l3cd(NMDedupMultiIndex        *multi_idx,
     for (i = 0; i < rdata->dns_domains_n; i++)
         nm_l3_config_data_add_search(l3cd, AF_INET6, rdata->dns_domains[i].domain);
 
+    if (rdata->pref64_n > 0) {
+        nm_l3_config_data_set_pref64(l3cd, rdata->pref64[0].prefix, rdata->pref64[0].plen);
+    } else {
+        nm_l3_config_data_set_pref64_valid(l3cd, FALSE);
+    }
+
     nm_l3_config_data_set_ndisc_hop_limit(l3cd, rdata->hop_limit);
     nm_l3_config_data_set_ndisc_reachable_time_msec(l3cd, rdata->reachable_time_ms);
     nm_l3_config_data_set_ndisc_retrans_timer_msec(l3cd, rdata->retrans_timer_ms);
 
-    nm_l3_config_data_set_ip6_mtu(l3cd, rdata->mtu);
+    nm_l3_config_data_set_ip6_mtu_ra(l3cd, rdata->mtu);
     if (token)
         nm_l3_config_data_set_ip6_token(l3cd, *token);
+    if (network_id)
+        nm_l3_config_data_set_network_id(l3cd, network_id);
 
     return g_steal_pointer(&l3cd);
 }
@@ -416,6 +426,7 @@ _data_complete(NMNDiscDataInternal *data)
     _SET(data, gateways);
     _SET(data, addresses);
     _SET(data, routes);
+    _SET(data, pref64);
     _SET(data, dns_servers);
     _SET(data, dns_domains);
 #undef _SET
@@ -437,7 +448,8 @@ nm_ndisc_emit_config_change(NMNDisc *self, NMNDiscConfigMap changed)
                                  nm_l3cfg_get_ifindex(priv->config.l3cfg),
                                  rdata,
                                  priv->config.ip6_privacy,
-                                 priv->iid_is_token ? &priv->iid : NULL);
+                                 priv->iid_is_token ? &priv->iid : NULL,
+                                 priv->config.network_id);
     l3cd = nm_l3_config_data_seal(l3cd);
 
     if (!nm_l3_config_data_equal(priv->l3cd, l3cd))
@@ -761,6 +773,59 @@ nm_ndisc_add_route(NMNDisc *ndisc, const NMNDiscRoute *new_item, gint64 now_msec
 }
 
 gboolean
+nm_ndisc_add_pref64(NMNDisc *ndisc, const NMNDiscPref64 *new_item, gint64 now_msec)
+{
+    NMNDiscDataInternal *rdata = &NM_NDISC_GET_PRIVATE(ndisc)->rdata;
+    guint                i;
+    guint                insert_idx = G_MAXUINT;
+
+    for (i = 0; i < rdata->pref64->len;) {
+        NMNDiscPref64 *item = &nm_g_array_index(rdata->pref64, NMNDiscPref64, i);
+
+        if (item->plen == new_item->plen && IN6_ARE_ADDR_EQUAL(&item->prefix, &new_item->prefix)
+            && IN6_ARE_ADDR_EQUAL(&item->gateway, &new_item->gateway)) {
+            if (new_item->expiry_msec <= now_msec) {
+                g_array_remove_index(rdata->pref64, i);
+                return TRUE;
+            }
+
+            if (item->gateway_preference != new_item->gateway_preference) {
+                g_array_remove_index(rdata->pref64, i);
+                continue;
+            }
+
+            item->gateway_expiry_msec = new_item->gateway_expiry_msec;
+
+            if (item->expiry_msec == new_item->expiry_msec)
+                return FALSE;
+
+            item->expiry_msec = new_item->expiry_msec;
+            return TRUE;
+        }
+
+        /* Put before less preferable gateways. */
+        if (_preference_to_priority(item->gateway_preference)
+                < _preference_to_priority(new_item->gateway_preference)
+            && insert_idx == G_MAXUINT)
+            insert_idx = i;
+
+        i++;
+    }
+
+    if (rdata->pref64->len >= _SIZE_MAX_PREF64)
+        return FALSE;
+
+    if (new_item->expiry_msec <= now_msec)
+        return FALSE;
+
+    g_array_insert_val(rdata->pref64,
+                       insert_idx == G_MAXUINT ? rdata->pref64->len : insert_idx,
+                       *new_item);
+
+    return TRUE;
+}
+
+gboolean
 nm_ndisc_add_dns_server(NMNDisc *ndisc, const NMNDiscDNSServer *new_item, gint64 now_msec)
 {
     NMNDiscPrivate      *priv;
@@ -1059,7 +1124,8 @@ nm_ndisc_set_config(NMNDisc *ndisc, const NML3ConfigData *l3cd)
     const NMPObject   *obj;
     guint              len;
     guint              i;
-    gint32             fake_now = NM_NDISC_EXPIRY_BASE_TIMESTAMP / 1000;
+    gint64             now_msec = nm_utils_get_monotonic_timestamp_msec();
+    gint32             now      = now_msec / 1000;
 
     nm_assert(NM_IS_NDISC(ndisc));
     nm_assert(nm_ndisc_get_node_type(ndisc) == NM_NDISC_NODE_TYPE_ROUTER);
@@ -1082,16 +1148,15 @@ nm_ndisc_set_config(NMNDisc *ndisc, const NML3ConfigData *l3cd)
         lifetime = nmp_utils_lifetime_get(addr->timestamp,
                                           addr->lifetime,
                                           addr->preferred,
-                                          &fake_now,
+                                          &now,
                                           &preferred);
         if (!lifetime)
             continue;
 
         a = (NMNDiscAddress) {
-            .address     = addr->address,
-            .expiry_msec = _nm_ndisc_lifetime_to_expiry(NM_NDISC_EXPIRY_BASE_TIMESTAMP, lifetime),
-            .expiry_preferred_msec =
-                _nm_ndisc_lifetime_to_expiry(NM_NDISC_EXPIRY_BASE_TIMESTAMP, preferred),
+            .address               = addr->address,
+            .expiry_msec           = _nm_ndisc_lifetime_to_expiry(now_msec, lifetime),
+            .expiry_preferred_msec = _nm_ndisc_lifetime_to_expiry(now_msec, preferred),
         };
 
         if (nm_ndisc_add_address(ndisc, &a, 0, FALSE))
@@ -1111,8 +1176,7 @@ nm_ndisc_set_config(NMNDisc *ndisc, const NML3ConfigData *l3cd)
 
         n = (NMNDiscDNSServer) {
             .address     = a.addr6,
-            .expiry_msec = _nm_ndisc_lifetime_to_expiry(NM_NDISC_EXPIRY_BASE_TIMESTAMP,
-                                                        NM_NDISC_ROUTER_LIFETIME),
+            .expiry_msec = _nm_ndisc_lifetime_to_expiry(now_msec, NM_NDISC_ROUTER_LIFETIME),
         };
         if (nm_ndisc_add_dns_server(ndisc, &n, G_MININT64))
             changed = TRUE;
@@ -1127,8 +1191,7 @@ nm_ndisc_set_config(NMNDisc *ndisc, const NML3ConfigData *l3cd)
 
         n = (NMNDiscDNSDomain) {
             .domain      = (char *) strvarr[i],
-            .expiry_msec = _nm_ndisc_lifetime_to_expiry(NM_NDISC_EXPIRY_BASE_TIMESTAMP,
-                                                        NM_NDISC_ROUTER_LIFETIME),
+            .expiry_msec = _nm_ndisc_lifetime_to_expiry(now_msec, NM_NDISC_ROUTER_LIFETIME),
         };
         if (nm_ndisc_add_dns_domain(ndisc, &n, G_MININT64))
             changed = TRUE;
@@ -1400,6 +1463,17 @@ _config_changed_log(NMNDisc *ndisc, NMNDiscConfigMap changed)
               nm_icmpv6_router_pref_to_string(route->preference, str_pref, sizeof(str_pref)),
               get_exp(str_exp, now_msec, route));
     }
+    for (i = 0; i < rdata->pref64->len; i++) {
+        const NMNDiscPref64 *pref64 = &nm_g_array_index(rdata->pref64, NMNDiscPref64, i);
+        char                 addrstr2[NM_INET_ADDRSTRLEN];
+
+        _LOGD("  pref64 %s/%u via %s exp %s",
+              nm_inet6_ntop(&pref64->prefix, addrstr),
+              pref64->plen,
+              nm_inet6_ntop(&pref64->gateway, addrstr2),
+              get_exp(str_exp, now_msec, pref64));
+    }
+
     for (i = 0; i < rdata->dns_servers->len; i++) {
         const NMNDiscDNSServer *dns_server =
             &nm_g_array_index(rdata->dns_servers, NMNDiscDNSServer, i);
@@ -1486,7 +1560,7 @@ clean_addresses(NMNDisc *ndisc, gint64 now_msec, NMNDiscConfigMap *changed, gint
     }
 
     if (i != j) {
-        *changed = NM_NDISC_CONFIG_ADDRESSES;
+        *changed |= NM_NDISC_CONFIG_ADDRESSES;
         g_array_set_size(rdata->addresses, j);
     }
 
@@ -1520,11 +1594,50 @@ clean_routes(NMNDisc *ndisc, gint64 now_msec, NMNDiscConfigMap *changed, gint64
         g_array_set_size(rdata->routes, j);
     }
 
-    if (_array_set_size_max(rdata->gateways, _SIZE_MAX_ROUTES))
+    if (_array_set_size_max(rdata->routes, _SIZE_MAX_ROUTES))
         *changed |= NM_NDISC_CONFIG_ROUTES;
 }
 
 static void
+clean_pref64(NMNDisc *ndisc, gint64 now_msec, NMNDiscConfigMap *changed, gint64 *next_msec)
+{
+    NMNDiscDataInternal *rdata = &NM_NDISC_GET_PRIVATE(ndisc)->rdata;
+    NMNDiscPref64       *arr;
+    guint                i;
+    guint                j;
+
+    if (rdata->pref64->len == 0)
+        return;
+
+    arr = &nm_g_array_first(rdata->pref64, NMNDiscPref64);
+
+    for (i = 0, j = 0; i < rdata->pref64->len; i++) {
+        if (!expiry_next(now_msec, arr[i].expiry_msec, next_msec)
+            || !expiry_next(now_msec,
+                            arr[i].gateway_expiry_msec,
+                            next_msec)) { /* no gateway no party */
+            if (i == 0) {
+                /* Emit the changed signal only when the first PREF64 expires,
+                 * because only the first item is exported into the l3cd. Changes
+                 * in other PREF64s are not relevant. */
+                *changed |= NM_NDISC_CONFIG_PREF64;
+            }
+            continue;
+        }
+
+        if (i != j)
+            arr[j] = arr[i];
+        j++;
+    }
+
+    if (i != j) {
+        g_array_set_size(rdata->pref64, j);
+    }
+
+    _array_set_size_max(rdata->pref64, _SIZE_MAX_PREF64);
+}
+
+static void
 clean_dns_servers(NMNDisc *ndisc, gint64 now_msec, NMNDiscConfigMap *changed, gint64 *next_msec)
 {
     NMNDiscDataInternal *rdata = &NM_NDISC_GET_PRIVATE(ndisc)->rdata;
@@ -1550,7 +1663,7 @@ clean_dns_servers(NMNDisc *ndisc, gint64 now_msec, NMNDiscConfigMap *changed, gi
         g_array_set_size(rdata->dns_servers, j);
     }
 
-    if (_array_set_size_max(rdata->gateways, _SIZE_MAX_DNS_SERVERS))
+    if (_array_set_size_max(rdata->dns_servers, _SIZE_MAX_DNS_SERVERS))
         *changed |= NM_NDISC_CONFIG_DNS_SERVERS;
 }
 
@@ -1580,12 +1693,12 @@ clean_dns_domains(NMNDisc *ndisc, gint64 now_msec, NMNDiscConfigMap *changed, gi
         j++;
     }
 
-    if (i != 0) {
+    if (i != j) {
         *changed |= NM_NDISC_CONFIG_DNS_DOMAINS;
         g_array_set_size(rdata->dns_domains, j);
     }
 
-    if (_array_set_size_max(rdata->gateways, _SIZE_MAX_DNS_DOMAINS))
+    if (_array_set_size_max(rdata->dns_domains, _SIZE_MAX_DNS_DOMAINS))
         *changed |= NM_NDISC_CONFIG_DNS_DOMAINS;
 }
 
@@ -1600,6 +1713,7 @@ check_timestamps(NMNDisc *ndisc, gint64 now_msec, NMNDiscConfigMap changed)
     clean_gateways(ndisc, now_msec, &changed, &next_msec);
     clean_addresses(ndisc, now_msec, &changed, &next_msec);
     clean_routes(ndisc, now_msec, &changed, &next_msec);
+    clean_pref64(ndisc, now_msec, &changed, &next_msec);
     clean_dns_servers(ndisc, now_msec, &changed, &next_msec);
     clean_dns_domains(ndisc, now_msec, &changed, &next_msec);
 
@@ -1919,6 +2033,7 @@ nm_ndisc_init(NMNDisc *ndisc)
     rdata->gateways    = g_array_new(FALSE, FALSE, sizeof(NMNDiscGateway));
     rdata->addresses   = g_array_new(FALSE, FALSE, sizeof(NMNDiscAddress));
     rdata->routes      = g_array_new(FALSE, FALSE, sizeof(NMNDiscRoute));
+    rdata->pref64      = g_array_new(FALSE, FALSE, sizeof(NMNDiscPref64));
     rdata->dns_servers = g_array_new(FALSE, FALSE, sizeof(NMNDiscDNSServer));
     rdata->dns_domains = g_array_new(FALSE, FALSE, sizeof(NMNDiscDNSDomain));
     g_array_set_clear_func(rdata->dns_domains, dns_domain_free);
@@ -1951,6 +2066,7 @@ finalize(GObject *object)
     g_array_unref(rdata->gateways);
     g_array_unref(rdata->addresses);
     g_array_unref(rdata->routes);
+    g_array_unref(rdata->pref64);
     g_array_unref(rdata->dns_servers);
     g_array_unref(rdata->dns_domains);
 
diff --git a/src/core/ndisc/nm-ndisc.h b/src/core/ndisc/nm-ndisc.h
index 8f1a12a2..cdd9a676 100644
--- a/src/core/ndisc/nm-ndisc.h
+++ b/src/core/ndisc/nm-ndisc.h
@@ -50,18 +50,6 @@ const char *nm_ndisc_dhcp_level_to_string(NMNDiscDHCPLevel level);
  * unit of it is milliseconds. But of course, infinity has not really a unit. */
 #define NM_NDISC_EXPIRY_INFINITY G_MAXINT64
 
-/* in common cases, the expiry_msec tracks the timestamp in nm_utils_get_monotonic_timestamp_mses()
- * timestamp when the item expires.
- *
- * When we configure an NMNDiscAddress to be announced via the router advertisement,
- * then that address does not have a fixed expiry point in time, instead, the expiry
- * really contains the lifetime from the moment when we send the router advertisement.
- * In that case, the expiry_msec is more a "lifetime" that starts counting at timestamp
- * zero.
- *
- * The unit is milliseconds (but of course, the timestamp is zero, so it doesn't really matter). */
-#define NM_NDISC_EXPIRY_BASE_TIMESTAMP ((gint64) 0)
-
 static inline gint64
 _nm_ndisc_lifetime_to_expiry(gint64 now_msec, guint32 lifetime)
 {
@@ -119,6 +107,15 @@ typedef struct _NMNDiscRoute {
     bool               duplicate : 1;
 } NMNDiscRoute;
 
+typedef struct _NMNDiscPref64 {
+    struct in6_addr    prefix;
+    struct in6_addr    gateway;
+    gint64             expiry_msec;
+    gint64             gateway_expiry_msec;
+    NMIcmpv6RouterPref gateway_preference;
+    guint8             plen;
+} NMNDiscPref64;
+
 typedef struct {
     struct in6_addr address;
     gint64          expiry_msec;
@@ -141,6 +138,7 @@ typedef enum {
     NM_NDISC_CONFIG_MTU            = 1 << 7,
     NM_NDISC_CONFIG_REACHABLE_TIME = 1 << 8,
     NM_NDISC_CONFIG_RETRANS_TIMER  = 1 << 9,
+    NM_NDISC_CONFIG_PREF64         = 1 << 10,
 } NMNDiscConfigMap;
 
 typedef enum {
@@ -188,12 +186,14 @@ typedef struct {
     guint gateways_n;
     guint addresses_n;
     guint routes_n;
+    guint pref64_n;
     guint dns_servers_n;
     guint dns_domains_n;
 
     const NMNDiscGateway   *gateways;
     const NMNDiscAddress   *addresses;
     const NMNDiscRoute     *routes;
+    const NMNDiscPref64    *pref64;
     const NMNDiscDNSServer *dns_servers;
     const NMNDiscDNSDomain *dns_domains;
 } NMNDiscData;
@@ -282,6 +282,7 @@ struct _NML3ConfigData *nm_ndisc_data_to_l3cd(NMDedupMultiIndex        *multi_id
                                               int                       ifindex,
                                               const NMNDiscData        *rdata,
                                               NMSettingIP6ConfigPrivacy ip6_privacy,
-                                              NMUtilsIPv6IfaceId       *token);
+                                              NMUtilsIPv6IfaceId       *token,
+                                              const char               *network_id);
 
 #endif /* __NETWORKMANAGER_NDISC_H__ */
diff --git a/src/core/nm-config.c b/src/core/nm-config.c
index d8bf2e3e..85e36ac8 100644
--- a/src/core/nm-config.c
+++ b/src/core/nm-config.c
@@ -892,6 +892,7 @@ static const ConfigGroup config_groups[] = {
         .is_prefix = TRUE,
         .keys      = NM_MAKE_STRV(NM_CONFIG_KEYFILE_KEY_DEVICE_CARRIER_WAIT_TIMEOUT,
                              NM_CONFIG_KEYFILE_KEY_DEVICE_IGNORE_CARRIER,
+                             NM_CONFIG_KEYFILE_KEY_DEVICE_CHECK_CONNECTIVITY,
                              NM_CONFIG_KEYFILE_KEY_DEVICE_MANAGED,
                              NM_CONFIG_KEYFILE_KEY_DEVICE_SRIOV_NUM_VFS,
                              NM_CONFIG_KEYFILE_KEY_DEVICE_KEEP_CONFIGURATION,
@@ -1256,34 +1257,28 @@ read_base_config(GKeyFile   *keyfile,
     return TRUE;
 }
 
+/* We want to use GDir instead of GFile here to avoid loading GVFS modules and
+ * initalizing DBUS infra for communicating with GVFS.
+ * https://redhat.atlassian.net/browse/RHEL-140113
+ */
 static GPtrArray *
 _get_config_dir_files(const char *config_dir)
 {
-    GFile           *dir;
-    GFileEnumerator *direnum;
-    GFileInfo       *info;
-    GPtrArray       *confs;
-    const char      *name;
-
+    GDir       *dir;
+    GPtrArray  *confs;
+    const char *name;
     g_return_val_if_fail(config_dir, NULL);
-
     confs = g_ptr_array_new_with_free_func(g_free);
     if (!*config_dir)
         return confs;
-
-    dir     = g_file_new_for_path(config_dir);
-    direnum = g_file_enumerate_children(dir, G_FILE_ATTRIBUTE_STANDARD_NAME, 0, NULL, NULL);
-    if (direnum) {
-        while ((info = g_file_enumerator_next_file(direnum, NULL, NULL))) {
-            name = g_file_info_get_name(info);
+    dir = g_dir_open(config_dir, 0, NULL);
+    if (dir) {
+        while ((name = g_dir_read_name(dir))) {
             if (NM_STR_HAS_SUFFIX(name, ".conf"))
                 g_ptr_array_add(confs, g_strdup(name));
-            g_object_unref(info);
         }
-        g_object_unref(direnum);
+        g_dir_close(dir);
     }
-    g_object_unref(dir);
-
     g_ptr_array_sort(confs, nm_strcmp_p);
     return confs;
 }
@@ -1339,8 +1334,7 @@ read_entire_config(const NMConfigCmdLineOptions *cli,
         run_config_dir = RUN_CONFIG_DIR;
 
     /* create a default configuration file. */
-    keyfile = nm_config_create_keyfile();
-
+    keyfile      = nm_config_create_keyfile();
     system_confs = _get_config_dir_files(system_config_dir);
     confs        = _get_config_dir_files(config_dir);
     run_confs    = _get_config_dir_files(run_config_dir);
@@ -3292,7 +3286,6 @@ init_sync(GInitable *initable, GCancellable *cancellable, GError **error)
         g_set_error(error, G_KEY_FILE_ERROR, G_KEY_FILE_ERROR_NOT_FOUND, "unspecified error");
         g_return_val_if_reached(FALSE);
     }
-
     s                = priv->cli.config_dir ?: "" DEFAULT_CONFIG_DIR;
     priv->config_dir = g_strdup(s[0] == '/' ? s : "");
 
@@ -3300,12 +3293,10 @@ init_sync(GInitable *initable, GCancellable *cancellable, GError **error)
     if (s[0] != '/' || nm_streq(s, priv->config_dir))
         s = "";
     priv->system_config_dir = g_strdup(s);
-
     if (priv->cli.intern_config_file)
         priv->intern_config_file = g_strdup(priv->cli.intern_config_file);
     else
         priv->intern_config_file = g_strdup(DEFAULT_INTERN_CONFIG_FILE);
-
     warnings = g_ptr_array_new_with_free_func(g_free);
 
     keyfile = read_entire_config(&priv->cli,
diff --git a/src/core/nm-core-utils.c b/src/core/nm-core-utils.c
index deac04e7..ee05bedd 100644
--- a/src/core/nm-core-utils.c
+++ b/src/core/nm-core-utils.c
@@ -21,6 +21,10 @@
 #include <linux/if_infiniband.h>
 #include <net/if_arp.h>
 #include <net/ethernet.h>
+#include <netinet/ip_icmp.h>
+#include <netinet/icmp6.h>
+#include <netinet/ip6.h>
+#include <linux/if_packet.h>
 
 #include "libnm-glib-aux/nm-uuid.h"
 #include "libnm-platform/nmp-base.h"
@@ -5003,6 +5007,469 @@ NM_UTILS_LOOKUP_STR_DEFINE(nm_activation_type_to_string,
 /*****************************************************************************/
 
 typedef struct {
+    NMIPAddrTyped address;
+    char         *addr_str;
+    GTask        *task;
+    GSource      *timeout_source;
+    GSource      *retry_source;
+    GSource      *input_source;
+    gulong        cancellable_id;
+    int           ifindex;
+    int           socket;
+    guint16       seq;
+} PingInfo;
+
+#define _NMLOG2_PREFIX_NAME "ping"
+#define _NMLOG2_DOMAIN      LOGD_CORE
+#define _NMLOG2(level, info, ...)                                                          \
+    G_STMT_START                                                                           \
+    {                                                                                      \
+        if (nm_logging_enabled((level), (_NMLOG2_DOMAIN))) {                               \
+            PingInfo *_info = (info);                                                      \
+                                                                                           \
+            _nm_log((level),                                                               \
+                    (_NMLOG2_DOMAIN),                                                      \
+                    0,                                                                     \
+                    NULL,                                                                  \
+                    NULL,                                                                  \
+                    _NMLOG2_PREFIX_NAME "[" NM_HASH_OBFUSCATE_PTR_FMT                      \
+                                        ",if=%d,%s]: " _NM_UTILS_MACRO_FIRST(__VA_ARGS__), \
+                    NM_HASH_OBFUSCATE_PTR(_info),                                          \
+                    _info->ifindex,                                                        \
+                    _info->addr_str _NM_UTILS_MACRO_REST(__VA_ARGS__));                    \
+        }                                                                                  \
+    }                                                                                      \
+    G_STMT_END
+
+static void
+ping_complete(PingInfo *info, GError *error)
+{
+    nm_clear_g_cancellable_disconnect(g_task_get_cancellable(info->task), &info->cancellable_id);
+
+    if (error && !nm_utils_error_is_cancelled(error)) {
+        _LOG2T(info, "terminated with error: %s", error->message);
+    }
+
+    if (error)
+        g_task_return_error(info->task, error);
+    else
+        g_task_return_boolean(info->task, TRUE);
+
+    nm_clear_g_source_inst(&info->timeout_source);
+    nm_clear_g_source_inst(&info->retry_source);
+    nm_clear_g_source_inst(&info->input_source);
+    nm_clear_g_free(&info->addr_str);
+    nm_clear_fd(&info->socket);
+    g_object_unref(info->task);
+
+    g_free(info);
+}
+
+static gboolean
+ping_socket_data_cb(int fd, GIOCondition condition, gpointer user_data)
+{
+    PingInfo *info = user_data;
+    ssize_t   len;
+    union {
+        struct icmphdr   icmph;
+        struct icmp6_hdr icmp6h;
+    } pkt;
+
+    len = recv(fd, &pkt, sizeof(pkt), 0);
+
+    if (len < 0)
+        return G_SOURCE_CONTINUE;
+
+    if (info->address.addr_family == AF_INET) {
+        if (len >= sizeof(struct icmphdr) && pkt.icmph.type == ICMP_ECHOREPLY) {
+            _LOG2T(info, "received echo-reply with seq %hu", ntohs(pkt.icmph.un.echo.sequence));
+            ping_complete(info, NULL);
+            return G_SOURCE_CONTINUE;
+        }
+    } else {
+        if (len >= sizeof(struct icmp6_hdr) && pkt.icmp6h.icmp6_type == ICMP6_ECHO_REPLY) {
+            _LOG2T(info, "received echo-reply with seq %hu", ntohs(pkt.icmp6h.icmp6_seq));
+            ping_complete(info, NULL);
+            return G_SOURCE_CONTINUE;
+        }
+    }
+
+    return G_SOURCE_CONTINUE;
+}
+
+static void
+ping_send(PingInfo *info)
+{
+    const bool IS_IPv4 = NM_IS_IPv4(info->address.addr_family);
+    union {
+        struct sockaddr_in6 sa6;
+        struct sockaddr_in  sa4;
+    } sa;
+    union {
+        struct icmphdr   icmph;
+        struct icmp6_hdr icmp6h;
+    } pkt;
+    socklen_t sa_len;
+    size_t    pkt_len;
+    nm_be32_t ifindex_be;
+    int       errsv;
+
+    info->seq++;
+
+    if (info->socket < 0) {
+        info->socket = socket(info->address.addr_family,
+                              SOCK_DGRAM | SOCK_CLOEXEC,
+                              IS_IPv4 ? IPPROTO_ICMP : IPPROTO_ICMPV6);
+        if (info->socket < 0) {
+            errsv = errno;
+            _LOG2T(info, "socket creation failed: %s", nm_strerror_native(errsv));
+            /* Try again at the next iteration */
+            return;
+        }
+
+        memset(&sa, 0, sizeof(sa));
+        if (IS_IPv4) {
+            sa.sa4.sin_family      = AF_INET;
+            sa.sa4.sin_addr.s_addr = info->address.addr.addr4;
+            sa_len                 = sizeof(struct sockaddr_in);
+        } else {
+            sa.sa6.sin6_family = AF_INET6;
+            sa.sa6.sin6_addr   = info->address.addr.addr6;
+            if (IN6_IS_ADDR_LINKLOCAL(&info->address.addr.addr6))
+                sa.sa6.sin6_scope_id = info->ifindex;
+            sa_len = sizeof(struct sockaddr_in6);
+        }
+
+        /* setsockopt(IP*_UNICAST_IF) must be called *before* connecting
+         * the socket, otherwise it doesn't have any effect */
+        ifindex_be = htonl(info->ifindex);
+        if (setsockopt(info->socket,
+                       IS_IPv4 ? IPPROTO_IP : IPPROTO_IPV6,
+                       IS_IPv4 ? IP_UNICAST_IF : IPV6_UNICAST_IF,
+                       &ifindex_be,
+                       sizeof(ifindex_be))) {
+            errsv = errno;
+            _LOG2T(info,
+                   "failed to bind the socket to the interface: %s",
+                   nm_strerror_native(errsv));
+            /* Try again at the next iteration */
+            nm_clear_fd(&info->socket);
+            return;
+        }
+
+        /* Connect the socket so that the kernel only delivers us packets
+         * coming from the given remote address */
+        if (connect(info->socket, (struct sockaddr *) &sa, sa_len) < 0) {
+            errsv = errno;
+            _LOG2T(info, "failed to connect the socket: %s", nm_strerror_native(errsv));
+            /* try again at the next iteration */
+            nm_clear_fd(&info->socket);
+            return;
+        }
+
+        info->input_source = nm_g_unix_fd_source_new(info->socket,
+                                                     G_IO_IN,
+                                                     G_PRIORITY_DEFAULT,
+                                                     ping_socket_data_cb,
+                                                     info,
+                                                     NULL);
+        g_source_attach(info->input_source, g_task_get_context(info->task));
+    }
+
+    if (IS_IPv4) {
+        memset(&pkt.icmph, 0, sizeof(struct icmphdr));
+        pkt.icmph.type             = ICMP_ECHO;
+        pkt.icmph.un.echo.sequence = htons(info->seq);
+        pkt_len                    = sizeof(struct icmphdr);
+    } else {
+        memset(&pkt.icmp6h, 0, sizeof(struct icmp6_hdr));
+        pkt.icmp6h.icmp6_type = ICMP6_ECHO_REQUEST;
+        pkt.icmp6h.icmp6_seq  = htons(info->seq);
+        pkt_len               = sizeof(struct icmp6_hdr);
+    }
+    /* The kernel will automatically set the ID ICMP field and filter
+     * incoming packets by the same ID */
+
+    if (send(info->socket, &pkt, pkt_len, 0) < 0) {
+        errsv = errno;
+        _LOG2T(info, "error sending echo-request #%u: %s", info->seq, nm_strerror_native(errsv));
+        return;
+    }
+
+    _LOG2T(info, "sent echo-request #%u", info->seq);
+}
+
+static gboolean
+ping_timeout_cb(gpointer user_data)
+{
+    PingInfo *info = user_data;
+
+    _LOG2T(info, "timeout");
+
+    nm_clear_g_source_inst(&info->timeout_source);
+    ping_complete(info, g_error_new_literal(NM_UTILS_ERROR, NM_UTILS_ERROR_UNKNOWN, "timeout"));
+
+    return G_SOURCE_CONTINUE;
+}
+
+static gboolean
+ping_retry_cb(gpointer user_data)
+{
+    PingInfo *info = user_data;
+
+    ping_send(info);
+
+    return G_SOURCE_CONTINUE;
+}
+
+static void
+ping_cancelled(GObject *object, gpointer user_data)
+{
+    PingInfo *info  = user_data;
+    GError   *error = NULL;
+
+    nm_clear_g_signal_handler(g_task_get_cancellable(info->task), &info->cancellable_id);
+    nm_utils_error_set_cancelled(&error, FALSE, NULL);
+    ping_complete(info, error);
+}
+
+void
+nm_utils_ping_host(NMIPAddrTyped       address,
+                   int                 ifindex,
+                   guint               timeout_sec,
+                   GCancellable       *cancellable,
+                   GAsyncReadyCallback callback,
+                   gpointer            cb_data)
+{
+    PingInfo *info;
+    char      buf[NM_INET_ADDRSTRLEN];
+    gulong    signal_id;
+
+    nm_assert(ifindex > 0);
+    nm_assert(G_IS_CANCELLABLE(cancellable));
+    nm_assert(callback);
+    nm_assert(cb_data);
+
+    info          = g_new0(PingInfo, 1);
+    info->address = address;
+    info->ifindex = ifindex;
+    info->task    = nm_g_task_new(NULL, cancellable, nm_utils_ping_host, callback, cb_data);
+    info->socket  = -1;
+
+    nm_inet_ntop(address.addr_family, address.addr.addr_ptr, buf);
+    info->addr_str = g_strdup(buf);
+
+    _LOG2T(info, "started");
+
+    if (timeout_sec > 0) {
+        info->timeout_source = nm_g_timeout_source_new_seconds(timeout_sec,
+                                                               G_PRIORITY_DEFAULT,
+                                                               ping_timeout_cb,
+                                                               info,
+                                                               NULL);
+        g_source_attach(info->timeout_source, g_task_get_context(info->task));
+    }
+
+    info->retry_source =
+        nm_g_timeout_source_new_seconds(1, G_PRIORITY_DEFAULT, ping_retry_cb, info, NULL);
+    g_source_attach(info->retry_source, g_task_get_context(info->task));
+
+    signal_id = g_cancellable_connect(cancellable, G_CALLBACK(ping_cancelled), info, NULL);
+    if (signal_id == 0) {
+        /* the callback was invoked synchronously, which destroyed @info.
+         * We must not touch it anymore. */
+        return;
+    }
+    info->cancellable_id = signal_id;
+
+    ping_send(info);
+}
+
+gboolean
+nm_utils_ping_host_finish(GAsyncResult *result, GError **error)
+{
+    GTask *task = G_TASK(result);
+
+    nm_assert(nm_g_task_is_valid(result, NULL, nm_utils_ping_host));
+
+    return g_task_propagate_boolean(task, error);
+}
+
+/*****************************************************************************/
+
+/*
+ * nm_utils_icmp6_checksum:
+ * @ip6_src: pointer to the IPv6 source address
+ * @data_len: length of the data
+ * @data: the data on which to compute the checksum
+ *
+ * Computes the ICMP6 checksum over @data (with length @data_len) and the IPv6
+ * pseudo-header. @ip6_src points to the source address in the IPv6 header.
+ */
+uint16_t
+nm_utils_icmp6_checksum(const void *ip6_src, size_t data_len, const void *data)
+{
+    uint32_t        sum = 0;
+    const uint16_t *ptr;
+    const uint8_t  *ptr8;
+    size_t          i;
+
+    /* Pseudo-header: source address */
+    ptr = (const uint16_t *) ip6_src;
+    for (i = 0; i < 8; i++)
+        sum += *ptr++;
+
+    /* Pseudo-header: destination address */
+    for (i = 0; i < 8; i++)
+        sum += *ptr++;
+
+    /* Pseudo-header: payload length */
+    sum += htons(data_len);
+
+    /* Pseudo-header: next header */
+    sum += htons(IPPROTO_ICMPV6);
+
+    /* ICMPv6 data */
+    ptr = (const uint16_t *) data;
+    for (i = 0; i < data_len / 2; i++)
+        sum += ptr[i];
+
+    /* Handle odd byte */
+    if (data_len % 2) {
+        ptr8 = &((const uint8_t *) data)[data_len - 1];
+        sum += htons((guint16) (*ptr8) << 8);
+    }
+
+    /* Fold 32-bit sum to 16 bits */
+    while (sum >> 16)
+        sum = (sum & 0xffff) + (sum >> 16);
+
+    return (uint16_t) ~sum;
+}
+
+/*
+ * nm_utils_ipv6_dad_send:
+ * @addr: the target IPv6 address
+ * @ifindex: the interface index
+ * @arptype: the ARP hardware type of the interface (e.g. ARPHRD_ETHER, ARPHRD_NONE)
+ *
+ * Send an IPv6 Duplicate Address Detection (DAD) Neighbor Solicitation
+ * for the given address.
+ *
+ * Returns: %TRUE if the packet was sent successfully, %FALSE on error
+ */
+gboolean
+nm_utils_ipv6_dad_send(const struct in6_addr *addr, int ifindex, int arptype)
+{
+    /* DAD packet: IPv6 header + ICMPv6 NS + nonce option (RFC 3971) */
+    struct _nm_packed {
+        struct ip6_hdr             ip6h;
+        struct nd_neighbor_solicit ns;
+        guint8                     ns_opt_nr;
+        guint8                     ns_opt_len;
+        guint8                     ns_opt_nonce[6];
+    } dad_pkt;
+    nm_auto_close int fd = -1;
+    int               errsv;
+    char              sbuf[NM_INET_ADDRSTRLEN];
+
+    nm_assert(addr);
+    nm_assert(ifindex > 0);
+
+    /* IPv6 header */
+    dad_pkt.ip6h = (struct ip6_hdr) {
+        .ip6_flow        = htonl(6 << 28), /* version 6, tclass 0, flowlabel 0 */
+        .ip6_plen        = htons(sizeof(dad_pkt) - sizeof(struct ip6_hdr)),
+        .ip6_nxt         = IPPROTO_ICMPV6,
+        .ip6_hlim        = 255,
+        .ip6_src         = IN6ADDR_ANY_INIT,
+        .ip6_dst.s6_addr = {0xff,
+                            0x02,
+                            0,
+                            0,
+                            0,
+                            0,
+                            0,
+                            0,
+                            0,
+                            0,
+                            0,
+                            0x01,
+                            0xff,
+                            addr->s6_addr[13],
+                            addr->s6_addr[14],
+                            addr->s6_addr[15]},
+    };
+
+    /* ICMPv6 Neighbor Solicitation */
+    dad_pkt.ns = (struct nd_neighbor_solicit) {
+        .nd_ns_type   = ND_NEIGHBOR_SOLICIT,
+        .nd_ns_target = *addr,
+    };
+
+    /* Nonce option (RFC 3971) */
+    dad_pkt.ns_opt_nr  = 14;
+    dad_pkt.ns_opt_len = 1; /* in units of 8 bytes */
+    nm_random_get_bytes(dad_pkt.ns_opt_nonce, sizeof(dad_pkt.ns_opt_nonce));
+
+    /* Compute the ICMPv6 checksum */
+    dad_pkt.ns.nd_ns_cksum = nm_utils_icmp6_checksum(&dad_pkt.ip6h.ip6_src,
+                                                     sizeof(dad_pkt) - sizeof(struct ip6_hdr),
+                                                     &dad_pkt.ns);
+
+    /* We need a ETH_P_IPV6 socket because we need to use a zero IPv6 source address */
+    fd = socket(AF_PACKET, SOCK_DGRAM | SOCK_CLOEXEC, htons(ETH_P_IPV6));
+    if (fd < 0) {
+        errsv = errno;
+        nm_log_warn(LOGD_CORE,
+                    "ipv6-dad: failed to create socket for %s: %s",
+                    nm_inet6_ntop(addr, sbuf),
+                    nm_strerror_native(errsv));
+        return FALSE;
+    }
+
+    /* Build link-layer destination address. For Ethernet, use the solicited-node
+     * multicast MAC address. For L3-only devices (ARPHRD_NONE, ARPHRD_RAWIP, etc.)
+     * there is no L2 header, so set sll_halen to 0. */
+    {
+        struct sockaddr_ll dst_ll = {
+            .sll_family   = AF_PACKET,
+            .sll_protocol = htons(ETH_P_IPV6),
+            .sll_ifindex  = ifindex,
+        };
+
+        if (arptype == ARPHRD_ETHER) {
+            dst_ll.sll_halen   = ETH_ALEN;
+            dst_ll.sll_addr[0] = 0x33;
+            dst_ll.sll_addr[1] = 0x33;
+            dst_ll.sll_addr[2] = 0xff;
+            dst_ll.sll_addr[3] = addr->s6_addr[13];
+            dst_ll.sll_addr[4] = addr->s6_addr[14];
+            dst_ll.sll_addr[5] = addr->s6_addr[15];
+        }
+
+        if (sendto(fd, &dad_pkt, sizeof(dad_pkt), 0, (struct sockaddr *) &dst_ll, sizeof(dst_ll))
+            < 0) {
+            errsv = errno;
+            nm_log_warn(LOGD_CORE,
+                        "ipv6-dad: failed to send DAD NS for %s: %s",
+                        nm_inet6_ntop(addr, sbuf),
+                        nm_strerror_native(errsv));
+            return FALSE;
+        }
+    }
+
+    nm_log_dbg(LOGD_CORE,
+               "ipv6-dad: sent DAD NS for %s on ifindex %d",
+               nm_inet6_ntop(addr, sbuf),
+               ifindex);
+
+    return TRUE;
+}
+
+/*****************************************************************************/
+
+typedef struct {
     GPid     pid;
     GTask   *task;
     gulong   cancellable_id;
@@ -5023,6 +5490,9 @@ typedef struct {
     gsize    out_buffer_offset;
 } HelperInfo;
 
+#undef _NMLOG2_PREFIX_NAME
+#undef _NMLOG2_DOMAIN
+#undef _NMLOG2
 #define _NMLOG2_PREFIX_NAME "nm-daemon-helper"
 #define _NMLOG2_DOMAIN      LOGD_CORE
 #define _NMLOG2(level, info, ...)                                                    \
diff --git a/src/core/nm-core-utils.h b/src/core/nm-core-utils.h
index cccccae6..a1892b99 100644
--- a/src/core/nm-core-utils.h
+++ b/src/core/nm-core-utils.h
@@ -304,6 +304,7 @@ typedef enum {
     NM_UTILS_STABLE_TYPE_STABLE_ID = 1,
     NM_UTILS_STABLE_TYPE_GENERATED = 2,
     NM_UTILS_STABLE_TYPE_RANDOM    = 3,
+    NM_UTILS_STABLE_TYPE_CLAT      = 4,
 } NMUtilsStableType;
 
 #define NM_UTILS_STABLE_TYPE_NONE ((NMUtilsStableType) - 1)
@@ -520,4 +521,19 @@ void        nm_utils_read_private_files(const char *const  *paths,
                                         gpointer            cb_data);
 GHashTable *nm_utils_read_private_files_finish(GAsyncResult *result, GError **error);
 
+/*****************************************************************************/
+
+void nm_utils_ping_host(NMIPAddrTyped       address,
+                        int                 ifindex,
+                        guint               timeout_sec,
+                        GCancellable       *cancellable,
+                        GAsyncReadyCallback callback,
+                        gpointer            cb_data);
+
+gboolean nm_utils_ping_host_finish(GAsyncResult *result, GError **error);
+
+uint16_t nm_utils_icmp6_checksum(const void *ip6_src, size_t data_len, const void *data);
+
+gboolean nm_utils_ipv6_dad_send(const struct in6_addr *addr, int ifindex, int arptype);
+
 #endif /* __NM_CORE_UTILS_H__ */
diff --git a/src/core/nm-ip-config.c b/src/core/nm-ip-config.c
index 75a75b42..4616f2e4 100644
--- a/src/core/nm-ip-config.c
+++ b/src/core/nm-ip-config.c
@@ -26,6 +26,7 @@ GType nm_ip6_config_get_type(void);
 /*****************************************************************************/
 
 #define NM_IP_CONFIG_ADDRESS_DATA "address-data"
+#define NM_IP_CONFIG_CLAT_ADDRESS "clat-address"
 #define NM_IP_CONFIG_DNS_OPTIONS  "dns-options"
 #define NM_IP_CONFIG_DNS_PRIORITY "dns-priority"
 #define NM_IP_CONFIG_DOMAINS      "domains"
@@ -41,6 +42,7 @@ NM_GOBJECT_PROPERTIES_DEFINE_FULL(_ip,
                                   NMIPConfig,
                                   PROP_IP_L3CFG,
                                   PROP_IP_ADDRESS_DATA,
+                                  PROP_IP_CLAT_ADDRESS,
                                   PROP_IP_GATEWAY,
                                   PROP_IP_ROUTE_DATA,
                                   PROP_IP_DOMAINS,
@@ -164,6 +166,8 @@ get_property_ip(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec
     const int          addr_family = nm_ip_config_get_addr_family(self);
     char             **to_free     = NULL;
     char               sbuf_addr[NM_INET_ADDRSTRLEN];
+    in_addr_t          addr4;
+    struct in6_addr    addr6;
     const char *const *strv;
     guint              len;
     int                v_i;
@@ -218,6 +222,20 @@ get_property_ip(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec
         strv = nm_l3_config_data_get_dns_options(priv->l3cd, addr_family, &len);
         _value_set_variant_as(value, strv, len);
         break;
+    case PROP_IP_CLAT_ADDRESS:
+        if (nm_l3_config_data_get_clat_state(priv->l3cd, &addr6, NULL, NULL, &addr4)) {
+            if (addr_family == AF_INET) {
+                g_value_set_variant(value,
+                                    g_variant_new_string(nm_inet_ntop(AF_INET, &addr4, sbuf_addr)));
+            } else {
+                g_value_set_variant(
+                    value,
+                    g_variant_new_string(nm_inet_ntop(AF_INET6, &addr6, sbuf_addr)));
+            }
+        } else {
+            g_value_set_variant(value, nm_g_variant_singleton_s_empty());
+        }
+        break;
     default:
         G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
         break;
@@ -336,6 +354,13 @@ nm_ip_config_class_init(NMIPConfigClass *klass)
                              G_VARIANT_TYPE("aa{sv}"),
                              NULL,
                              G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+    obj_properties_ip[PROP_IP_CLAT_ADDRESS] =
+        g_param_spec_variant(NM_IP_CONFIG_CLAT_ADDRESS,
+                             "",
+                             "",
+                             G_VARIANT_TYPE("s"),
+                             NULL,
+                             G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
     obj_properties_ip[PROP_IP_GATEWAY] =
         g_param_spec_variant(NM_IP_CONFIG_GATEWAY,
                              "",
@@ -512,6 +537,9 @@ static const NMDBusInterfaceInfoExtended interface_info_ip4_config = {
             NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("AddressData",
                                                            "aa{sv}",
                                                            NM_IP_CONFIG_ADDRESS_DATA),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("ClatAddress",
+                                                           "s",
+                                                           NM_IP_CONFIG_CLAT_ADDRESS),
             NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("Gateway", "s", NM_IP_CONFIG_GATEWAY),
             NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE(
                 "Routes",
@@ -614,6 +642,7 @@ nm_ip4_config_class_init(NMIP4ConfigClass *klass)
 /*****************************************************************************/
 
 /* public */
+#define NM_IP6_CONFIG_CLAT_PREF64 "clat-pref64"
 #define NM_IP6_CONFIG_NAMESERVERS "nameservers"
 
 /* deprecated */
@@ -625,6 +654,7 @@ typedef struct _NMIP6ConfigClass NMIP6ConfigClass;
 
 NM_GOBJECT_PROPERTIES_DEFINE_FULL(_ip6,
                                   NMIP6Config,
+                                  PROP_IP6_CLAT_PREF64,
                                   PROP_IP6_NAMESERVERS,
                                   PROP_IP6_ADDRESSES,
                                   PROP_IP6_ROUTES, );
@@ -651,6 +681,12 @@ static const NMDBusInterfaceInfoExtended interface_info_ip6_config = {
             NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("AddressData",
                                                            "aa{sv}",
                                                            NM_IP_CONFIG_ADDRESS_DATA),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("ClatAddress",
+                                                           "s",
+                                                           NM_IP_CONFIG_CLAT_ADDRESS),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("ClatPref64",
+                                                           "s",
+                                                           NM_IP6_CONFIG_CLAT_PREF64),
             NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("Gateway", "s", NM_IP_CONFIG_GATEWAY),
             NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE(
                 "Routes",
@@ -682,11 +718,24 @@ get_property_ip6(GObject *object, guint prop_id, GValue *value, GParamSpec *pspe
     guint              len;
     guint              i;
     const char *const *strarr;
+    guint8             plen;
+    struct in6_addr    addr6;
+    char               sbuf[NM_UTILS_INET_ADDRSTRLEN];
 
     switch (prop_id) {
     case PROP_IP6_ADDRESSES:
         g_value_set_variant(value, priv->v_addresses);
         break;
+    case PROP_IP6_CLAT_PREF64:
+        if (nm_l3_config_data_get_clat_state(priv->l3cd, NULL, &addr6, &plen, NULL)) {
+            nm_inet6_ntop(&addr6, sbuf);
+            g_value_set_variant(value,
+                                g_variant_new_string(
+                                    nm_sprintf_bufa(NM_INET_ADDRSTRLEN + 32, "%s/%u", sbuf, plen)));
+        } else {
+            g_value_set_variant(value, nm_g_variant_singleton_s_empty());
+        }
+        break;
     case PROP_IP6_ROUTES:
         g_value_set_variant(value, priv->v_routes);
         break;
@@ -740,6 +789,13 @@ nm_ip6_config_class_init(NMIP6ConfigClass *klass)
                              G_VARIANT_TYPE("a(ayuay)"),
                              NULL,
                              G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+    obj_properties_ip6[PROP_IP6_CLAT_PREF64] =
+        g_param_spec_variant(NM_IP6_CONFIG_CLAT_PREF64,
+                             "",
+                             "",
+                             G_VARIANT_TYPE("s"),
+                             NULL,
+                             G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
     obj_properties_ip6[PROP_IP6_ROUTES] =
         g_param_spec_variant(NM_IP6_CONFIG_ROUTES,
                              "",
@@ -787,7 +843,7 @@ _handle_l3cd_changed(NMIPConfig *self, const NML3ConfigData *l3cd)
     const int                                IS_IPv4     = NM_IS_IPv4(addr_family);
     NMIPConfigPrivate                       *priv        = NM_IP_CONFIG_GET_PRIVATE(self);
     nm_auto_unref_l3cd const NML3ConfigData *l3cd_old    = NULL;
-    GParamSpec                              *changed_params[8];
+    GParamSpec                              *changed_params[10];
     guint                                    n_changed_params = 0;
     const char *const                       *strarr;
     const char *const                       *strarr_old;
@@ -840,6 +896,59 @@ _handle_l3cd_changed(NMIPConfig *self, const NML3ConfigData *l3cd)
         }
     }
 
+    /* CLAT state */
+    {
+        struct in6_addr clat_ip6;
+        struct in6_addr clat_pref64;
+        guint8          clat_pref64_plen;
+        in_addr_t       clat_ip4;
+        gboolean        clat_enabled;
+        struct in6_addr clat_ip6_old;
+        struct in6_addr clat_pref64_old;
+        guint8          clat_pref64_plen_old;
+        in_addr_t       clat_ip4_old;
+        gboolean        clat_enabled_old;
+        gboolean        changed;
+
+        clat_enabled_old = nm_l3_config_data_get_clat_state(l3cd_old,
+                                                            &clat_ip6_old,
+                                                            &clat_pref64_old,
+                                                            &clat_pref64_plen_old,
+                                                            &clat_ip4_old);
+        clat_enabled     = nm_l3_config_data_get_clat_state(priv->l3cd,
+                                                        &clat_ip6,
+                                                        &clat_pref64,
+                                                        &clat_pref64_plen,
+                                                        &clat_ip4);
+
+        /* CLAT address */
+        if (clat_enabled != clat_enabled_old) {
+            changed = TRUE;
+        } else if (!clat_enabled) {
+            changed = FALSE;
+        } else if (IS_IPv4) {
+            changed = (clat_ip4 != clat_ip4_old);
+        } else {
+            changed = !IN6_ARE_ADDR_EQUAL(&clat_ip6, &clat_ip6_old);
+        }
+        if (changed)
+            changed_params[n_changed_params++] = obj_properties_ip[PROP_IP_CLAT_ADDRESS];
+
+        /*  PREF64 */
+        if (!IS_IPv4) {
+            if (clat_enabled != clat_enabled_old) {
+                changed = TRUE;
+            } else if (!clat_enabled) {
+                changed = FALSE;
+            } else {
+                changed = (clat_pref64_plen != clat_pref64_plen_old)
+                          || (!IN6_ARE_ADDR_EQUAL(&clat_pref64, &clat_pref64_old));
+            }
+            if (changed)
+                changed_params[n_changed_params++] = obj_properties_ip6[PROP_IP6_CLAT_PREF64];
+        }
+    }
+
     _notify_all(self, changed_params, n_changed_params);
 }
 
diff --git a/src/core/nm-l3-config-data.c b/src/core/nm-l3-config-data.c
index 7500337e..849da7f4 100644
--- a/src/core/nm-l3-config-data.c
+++ b/src/core/nm-l3-config-data.c
@@ -50,6 +50,9 @@ struct _NML3ConfigData {
         const NMPObject *best_default_route_x[2];
     };
 
+    struct in6_addr pref64_prefix;
+    guint32         pref64_plen;
+
     GArray *wins;
     GArray *nis_servers;
 
@@ -122,6 +125,19 @@ struct _NML3ConfigData {
     NMSettingConnectionDnsOverTls dns_over_tls;
     NMSettingConnectionDnssec     dnssec;
     NMUtilsIPv6IfaceId            ip6_token;
+    NMRefString                  *network_id;
+    NMSettingIp4ConfigClat        clat_config; /* this indicates the 'administrative' CLAT
+                                                * state, i.e. whether CLAT will be started
+                                                * once we receive a PREF64 */
+
+    /* The runtime CLAT state */
+    struct {
+        struct in6_addr ip6;
+        struct in6_addr pref64;
+        in_addr_t       ip4;
+        guint8          pref64_plen;
+        bool            enabled;
+    } clat_state;
 
     NML3ConfigDatFlags flags;
 
@@ -130,7 +146,8 @@ struct _NML3ConfigData {
     int ndisc_hop_limit_val;
 
     guint32 mtu;
-    guint32 ip6_mtu;
+    guint32 ip6_mtu_static; /* IPv6 MTU from the connection profile */
+    guint32 ip6_mtu_ra;     /* IPv6 MTU from Router Advertisement */
     guint32 ndisc_reachable_time_msec_val;
     guint32 ndisc_retrans_timer_msec_val;
 
@@ -167,6 +184,8 @@ struct _NML3ConfigData {
 
     bool routed_dns_4 : 1;
     bool routed_dns_6 : 1;
+
+    bool pref64_valid : 1;
 };
 
 /*****************************************************************************/
@@ -390,8 +409,11 @@ nm_l3_config_data_log(const NML3ConfigData *self,
            : "",
        !self->is_sealed ? ", not-sealed" : "");
 
-    if (self->mtu != 0 || self->ip6_mtu != 0) {
-        _L("mtu: %u, ip6-mtu: %u", self->mtu, self->ip6_mtu);
+    if (self->mtu != 0 || self->ip6_mtu_static != 0 || self->ip6_mtu_ra != 0) {
+        _L("mtu: %u, ip6-mtu-static: %u, ip6-mtu-ra %u",
+           self->mtu,
+           self->ip6_mtu_static,
+           self->ip6_mtu_ra);
     }
 
     for (IS_IPv4 = 1; IS_IPv4 >= 0; IS_IPv4--) {
@@ -519,6 +541,27 @@ nm_l3_config_data_log(const NML3ConfigData *self,
                 _L("nis-domain: %s", self->nis_domain->str);
         }
 
+        if (!IS_IPv4) {
+            if (self->clat_config == NM_SETTING_IP4_CONFIG_CLAT_AUTO)
+                _L("clat-config: auto");
+            else if (self->clat_config == NM_SETTING_IP4_CONFIG_CLAT_FORCE)
+                _L("clat-config: force");
+
+            if (self->clat_state.enabled) {
+                _L("clat-state: ip4=%s/32, pref64=%s/%u, ip6=%s/64",
+                   nm_inet4_ntop(self->clat_state.ip4, sbuf + NM_INET_ADDRSTRLEN),
+                   nm_inet6_ntop(&self->clat_state.pref64, sbuf),
+                   self->clat_state.pref64_plen,
+                   nm_inet6_ntop(&self->clat_state.ip6, sbuf_addr));
+            }
+        }
+
+        if (!IS_IPv4 && self->pref64_valid) {
+            _L("pref64_prefix: %s/%d",
+               nm_utils_inet6_ntop(&self->pref64_prefix, sbuf_addr),
+               self->pref64_plen);
+        }
+
         if (self->dhcp_lease_x[IS_IPv4]) {
             gs_free NMUtilsNamedValue *options_free = NULL;
             NMUtilsNamedValue          options_buffer[30];
@@ -603,6 +646,10 @@ nm_l3_config_data_log(const NML3ConfigData *self,
            nm_utils_inet6_interface_identifier_to_token(&self->ip6_token, sbuf_addr));
     }
 
+    if (self->network_id) {
+        _L("network-id: %s", self->network_id->str);
+    }
+
     if (self->metered != NM_TERNARY_DEFAULT)
         _L("metered: %s", self->metered ? "yes" : "no");
 
@@ -709,6 +756,7 @@ nm_l3_config_data_new(NMDedupMultiIndex *multi_idx, int ifindex, NMIPConfigSourc
         .flags                          = NM_L3_CONFIG_DAT_FLAGS_NONE,
         .metered                        = NM_TERNARY_DEFAULT,
         .proxy_browser_only             = NM_TERNARY_DEFAULT,
+        .clat_config                    = NM_SETTING_IP4_CONFIG_CLAT_NO,
         .proxy_method                   = NM_PROXY_CONFIG_METHOD_UNKNOWN,
         .route_table_sync_4             = NM_IP_ROUTE_TABLE_SYNC_MODE_NONE,
         .route_table_sync_6             = NM_IP_ROUTE_TABLE_SYNC_MODE_NONE,
@@ -722,6 +770,7 @@ nm_l3_config_data_new(NMDedupMultiIndex *multi_idx, int ifindex, NMIPConfigSourc
         .ndisc_retrans_timer_msec_set   = FALSE,
         .allow_routes_without_address_4 = TRUE,
         .allow_routes_without_address_6 = TRUE,
+        .pref64_valid                   = FALSE,
     };
 
     _idx_type_init(&self->idx_addresses_4, NMP_OBJECT_TYPE_IP4_ADDRESS);
@@ -822,6 +871,7 @@ nm_l3_config_data_unref(const NML3ConfigData *self)
     nm_ref_string_unref(mutable->nis_domain);
     nm_ref_string_unref(mutable->proxy_pac_url);
     nm_ref_string_unref(mutable->proxy_pac_script);
+    nm_ref_string_unref(mutable->network_id);
 
     nm_g_slice_free(mutable);
 }
@@ -1890,22 +1940,42 @@ nm_l3_config_data_set_mtu(NML3ConfigData *self, guint32 mtu)
 }
 
 guint32
-nm_l3_config_data_get_ip6_mtu(const NML3ConfigData *self)
+nm_l3_config_data_get_ip6_mtu_static(const NML3ConfigData *self)
+{
+    nm_assert(_NM_IS_L3_CONFIG_DATA(self, TRUE));
+
+    return self->ip6_mtu_static;
+}
+
+gboolean
+nm_l3_config_data_set_ip6_mtu_static(NML3ConfigData *self, guint32 ip6_mtu)
+{
+    nm_assert(_NM_IS_L3_CONFIG_DATA(self, FALSE));
+
+    if (self->ip6_mtu_static == ip6_mtu)
+        return FALSE;
+
+    self->ip6_mtu_static = ip6_mtu;
+    return TRUE;
+}
+
+guint32
+nm_l3_config_data_get_ip6_mtu_ra(const NML3ConfigData *self)
 {
     nm_assert(_NM_IS_L3_CONFIG_DATA(self, TRUE));
 
-    return self->ip6_mtu;
+    return self->ip6_mtu_ra;
 }
 
 gboolean
-nm_l3_config_data_set_ip6_mtu(NML3ConfigData *self, guint32 ip6_mtu)
+nm_l3_config_data_set_ip6_mtu_ra(NML3ConfigData *self, guint32 ip6_mtu)
 {
     nm_assert(_NM_IS_L3_CONFIG_DATA(self, FALSE));
 
-    if (self->ip6_mtu == ip6_mtu)
+    if (self->ip6_mtu_ra == ip6_mtu)
         return FALSE;
 
-    self->ip6_mtu = ip6_mtu;
+    self->ip6_mtu_ra = ip6_mtu;
     return TRUE;
 }
 
@@ -1957,6 +2027,132 @@ nm_l3_config_data_set_ip6_token(NML3ConfigData *self, NMUtilsIPv6IfaceId ipv6_to
     return TRUE;
 }
 
+const char *
+nm_l3_config_data_get_network_id(const NML3ConfigData *self)
+{
+    nm_assert(_NM_IS_L3_CONFIG_DATA(self, TRUE));
+
+    return nm_ref_string_get_str(self->network_id);
+}
+
+gboolean
+nm_l3_config_data_set_network_id(NML3ConfigData *self, const char *value)
+{
+    nm_assert(_NM_IS_L3_CONFIG_DATA(self, FALSE));
+
+    return nm_ref_string_reset_str(&self->network_id, value);
+}
+
+gboolean
+nm_l3_config_data_set_clat_config(NML3ConfigData *self, NMSettingIp4ConfigClat val)
+{
+    nm_assert(_NM_IS_L3_CONFIG_DATA(self, FALSE));
+    nm_assert(NM_IN_SET(val,
+                        NM_SETTING_IP4_CONFIG_CLAT_NO,
+                        NM_SETTING_IP4_CONFIG_CLAT_FORCE,
+                        NM_SETTING_IP4_CONFIG_CLAT_AUTO));
+
+    if (self->clat_config == val)
+        return FALSE;
+    self->clat_config = val;
+    return TRUE;
+}
+
+NMSettingIp4ConfigClat
+nm_l3_config_data_get_clat_config(const NML3ConfigData *self)
+{
+    nm_assert(_NM_IS_L3_CONFIG_DATA(self, TRUE));
+
+    return self->clat_config;
+}
+
+gboolean
+nm_l3_config_data_get_clat_state(const NML3ConfigData *self,
+                                 struct in6_addr      *out_ip6,
+                                 struct in6_addr      *out_pref64,
+                                 guint8               *out_pref64_plen,
+                                 in_addr_t            *out_ip4)
+{
+    if (!self || !self->clat_state.enabled)
+        return FALSE;
+    NM_SET_OUT(out_ip6, self->clat_state.ip6);
+    NM_SET_OUT(out_pref64, self->clat_state.pref64);
+    NM_SET_OUT(out_pref64_plen, self->clat_state.pref64_plen);
+    NM_SET_OUT(out_ip4, self->clat_state.ip4);
+    return TRUE;
+}
+
+void
+nm_l3_config_data_set_clat_state(NML3ConfigData        *self,
+                                 gboolean               enabled,
+                                 const struct in6_addr *ip6,
+                                 const struct in6_addr *pref64,
+                                 guint8                 pref64_plen,
+                                 in_addr_t              ip4)
+{
+    nm_assert(_NM_IS_L3_CONFIG_DATA(self, FALSE));
+
+    self->clat_state.enabled = enabled;
+    if (enabled) {
+        self->clat_state.ip6         = *ip6;
+        self->clat_state.pref64      = *pref64;
+        self->clat_state.pref64_plen = pref64_plen;
+        self->clat_state.ip4         = ip4;
+    } else {
+        self->clat_state.ip6         = in6addr_any;
+        self->clat_state.pref64      = in6addr_any;
+        self->clat_state.pref64_plen = 0;
+        self->clat_state.ip4         = 0;
+    }
+}
+
+gboolean
+nm_l3_config_data_set_pref64_valid(NML3ConfigData *self, gboolean val)
+{
+    if (self->pref64_valid == val)
+        return FALSE;
+    self->pref64_valid = val;
+    return TRUE;
+}
+
+gboolean
+nm_l3_config_data_get_pref64_valid(const NML3ConfigData *self)
+{
+    nm_assert(_NM_IS_L3_CONFIG_DATA(self, TRUE));
+
+    return self->pref64_valid;
+}
+
+gboolean
+nm_l3_config_data_get_pref64(const NML3ConfigData *self,
+                             struct in6_addr      *out_prefix,
+                             guint32              *out_plen)
+{
+    nm_assert(_NM_IS_L3_CONFIG_DATA(self, TRUE));
+
+    if (!self->pref64_valid)
+        return FALSE;
+    NM_SET_OUT(out_prefix, self->pref64_prefix);
+    NM_SET_OUT(out_plen, self->pref64_plen);
+    return TRUE;
+}
+
+gboolean
+nm_l3_config_data_set_pref64(NML3ConfigData *self, struct in6_addr prefix, guint32 plen)
+{
+    if (self->pref64_valid) {
+        if (self->pref64_plen == plen
+            && nm_ip6_addr_same_prefix(&self->pref64_prefix, &prefix, plen)) {
+            return FALSE;
+        }
+    } else {
+        self->pref64_valid = TRUE;
+    }
+    self->pref64_prefix = prefix;
+    self->pref64_plen   = plen;
+    return TRUE;
+}
+
 NMMptcpFlags
 nm_l3_config_data_get_mptcp_flags(const NML3ConfigData *self)
 {
@@ -2294,8 +2490,8 @@ _dedup_multi_index_cmp(const NML3ConfigData *a,
             switch (obj_type) {
             case NMP_OBJECT_TYPE_IP4_ADDRESS:
                 NM_CMP_DIRECT(obj_a->ip4_address.plen, obj_b->ip4_address.plen);
-                NM_CMP_DIRECT(obj_b->ip4_address.address, obj_b->ip4_address.address);
-                NM_CMP_DIRECT(obj_b->ip4_address.peer_address, obj_b->ip4_address.peer_address);
+                NM_CMP_DIRECT(obj_a->ip4_address.address, obj_b->ip4_address.address);
+                NM_CMP_DIRECT(obj_a->ip4_address.peer_address, obj_b->ip4_address.peer_address);
                 break;
             case NMP_OBJECT_TYPE_IP6_ADDRESS:
                 NM_CMP_DIRECT(obj_a->ip6_address.plen, obj_b->ip6_address.plen);
@@ -2408,15 +2604,15 @@ nm_l3_config_data_cmp_full(const NML3ConfigData *a,
         const NMPObject *def_route_a = a->best_default_route_x[IS_IPv4];
         const NMPObject *def_route_b = b->best_default_route_x[IS_IPv4];
 
-        if (def_route_a != def_route_b) {
-            if (NM_FLAGS_HAS(flags, NM_L3_CONFIG_CMP_FLAGS_ROUTES)) {
-                NM_CMP_RETURN(
-                    nmp_object_cmp_full(def_route_a,
-                                        def_route_b,
-                                        NM_FLAGS_HAS(flags, NM_L3_CONFIG_CMP_FLAGS_IFINDEX)
-                                            ? NMP_OBJECT_CMP_FLAGS_NONE
-                                            : NMP_OBJECT_CMP_FLAGS_IGNORE_IFINDEX));
-            } else if (NM_FLAGS_HAS(flags, NM_L3_CONFIG_CMP_FLAGS_ROUTES_ID)) {
+        if (NM_FLAGS_HAS(flags, NM_L3_CONFIG_CMP_FLAGS_ROUTES)) {
+            NM_CMP_RETURN(nmp_object_cmp_full(def_route_a,
+                                              def_route_b,
+                                              NM_FLAGS_HAS(flags, NM_L3_CONFIG_CMP_FLAGS_IFINDEX)
+                                                  ? NMP_OBJECT_CMP_FLAGS_NONE
+                                                  : NMP_OBJECT_CMP_FLAGS_IGNORE_IFINDEX));
+        } else if (NM_FLAGS_HAS(flags, NM_L3_CONFIG_CMP_FLAGS_ROUTES_ID)) {
+            NM_CMP_DIRECT(!!def_route_a, !!def_route_b);
+            if (def_route_a && def_route_b) {
                 if (NM_FLAGS_HAS(flags, NM_L3_CONFIG_CMP_FLAGS_IFINDEX)) {
                     NM_CMP_DIRECT(def_route_a->obj_with_ifindex.ifindex,
                                   def_route_b->obj_with_ifindex.ifindex);
@@ -2484,8 +2680,10 @@ nm_l3_config_data_cmp_full(const NML3ConfigData *a,
     if (NM_FLAGS_HAS(flags, NM_L3_CONFIG_CMP_FLAGS_OTHER)) {
         NM_CMP_DIRECT(a->flags, b->flags);
         NM_CMP_DIRECT(a->ip6_token.id, b->ip6_token.id);
+        NM_CMP_DIRECT_REF_STRING(a->network_id, b->network_id);
         NM_CMP_DIRECT(a->mtu, b->mtu);
-        NM_CMP_DIRECT(a->ip6_mtu, b->ip6_mtu);
+        NM_CMP_DIRECT(a->ip6_mtu_static, b->ip6_mtu_static);
+        NM_CMP_DIRECT(a->ip6_mtu_ra, b->ip6_mtu_ra);
         NM_CMP_DIRECT_UNSAFE(a->metered, b->metered);
         NM_CMP_DIRECT_UNSAFE(a->proxy_browser_only, b->proxy_browser_only);
         NM_CMP_DIRECT_UNSAFE(a->proxy_method, b->proxy_method);
@@ -2509,6 +2707,23 @@ nm_l3_config_data_cmp_full(const NML3ConfigData *a,
         NM_CMP_DIRECT_UNSAFE(a->routed_dns_4, b->routed_dns_4);
         NM_CMP_DIRECT_UNSAFE(a->routed_dns_6, b->routed_dns_6);
 
+        NM_CMP_DIRECT_UNSAFE(a->clat_config, b->clat_config);
+
+        NM_CMP_DIRECT(!!a->clat_state.enabled, !!b->clat_state.enabled);
+        if (a->clat_state.enabled) {
+            NM_CMP_DIRECT_IN6ADDR(&a->clat_state.ip6, &b->clat_state.ip6);
+            NM_CMP_DIRECT_IN6ADDR(&a->clat_state.pref64, &b->clat_state.pref64);
+            NM_CMP_DIRECT(a->clat_state.pref64_plen, b->clat_state.pref64_plen);
+            NM_CMP_DIRECT(a->clat_state.ip4, b->clat_state.ip4);
+        }
+
+        NM_CMP_DIRECT(!!a->pref64_valid, !!b->pref64_valid);
+        if (a->pref64_valid) {
+            NM_CMP_DIRECT(a->pref64_plen, b->pref64_plen);
+            NM_CMP_RETURN_DIRECT(
+                nm_ip6_addr_same_prefix_cmp(&a->pref64_prefix, &b->pref64_prefix, a->pref64_plen));
+        }
+
         NM_CMP_FIELD(a, b, source);
     }
 
@@ -2524,8 +2739,10 @@ nm_l3_config_data_cmp_full(const NML3ConfigData *a,
 
 /*****************************************************************************/
 
-static const NMPObject *
-_data_get_direct_route_for_host(const NML3ConfigData *self, int addr_family, gconstpointer host)
+const NMPObject *
+nm_l3_config_data_get_direct_route_for_host(const NML3ConfigData *self,
+                                            int                   addr_family,
+                                            gconstpointer         host)
 {
     const int                 IS_IPv4        = NM_IS_IPv4(addr_family);
     const NMPObject          *best_route_obj = NULL;
@@ -2556,7 +2773,8 @@ _data_get_direct_route_for_host(const NML3ConfigData *self, int addr_family, gco
         if (!nm_ip_addr_same_prefix(addr_family, host, item->rx.network_ptr, item->rx.plen))
             continue;
 
-        if (best_route && best_route->rx.metric <= item->rx.metric)
+        if (best_route && best_route->rx.plen == item->rx.plen
+            && best_route->rx.metric <= item->rx.metric)
             continue;
 
         best_route_obj = item_obj;
@@ -2683,7 +2901,7 @@ nm_l3_config_data_add_dependent_onlink_routes(NML3ConfigData *self, int addr_fam
         if (NM_FLAGS_HAS(route_src->rx.r_rtm_flags, (unsigned) RTNH_F_ONLINK))
             continue;
 
-        if (_data_get_direct_route_for_host(self, addr_family, p_gateway))
+        if (nm_l3_config_data_get_direct_route_for_host(self, addr_family, p_gateway))
             continue;
 
         new_route = nmp_object_clone(obj_src, FALSE);
@@ -3027,6 +3245,9 @@ _init_from_connection_ip(NML3ConfigData *self, int addr_family, NMConnection *co
         nm_l3_config_data_set_ip6_privacy(
             self,
             nm_setting_ip6_config_get_ip6_privacy(NM_SETTING_IP6_CONFIG(s_ip)));
+        nm_l3_config_data_set_ip6_mtu_static(
+            self,
+            nm_setting_ip6_config_get_mtu(NM_SETTING_IP6_CONFIG(s_ip)));
     }
 }
 
@@ -3503,6 +3724,9 @@ nm_l3_config_data_merge(NML3ConfigData       *self,
     if (self->ip6_token.id == 0)
         self->ip6_token.id = src->ip6_token.id;
 
+    if (!self->network_id)
+        self->network_id = nm_ref_string_ref(src->network_id);
+
     self->metered = NM_MAX((NMTernary) self->metered, (NMTernary) src->metered);
 
     if (self->proxy_method == NM_PROXY_CONFIG_METHOD_UNKNOWN)
@@ -3541,8 +3765,11 @@ nm_l3_config_data_merge(NML3ConfigData       *self,
     if (self->mtu == 0u)
         self->mtu = src->mtu;
 
-    if (self->ip6_mtu == 0u)
-        self->ip6_mtu = src->ip6_mtu;
+    if (self->ip6_mtu_static == 0u)
+        self->ip6_mtu_static = src->ip6_mtu_static;
+
+    if (self->ip6_mtu_ra == 0u)
+        self->ip6_mtu_ra = src->ip6_mtu_ra;
 
     if (NM_FLAGS_HAS(merge_flags, NM_L3_CONFIG_MERGE_FLAGS_CLONE)) {
         _nm_unused nm_auto_unref_dhcplease NMDhcpLease *dhcp_lease_6 =
@@ -3564,6 +3791,24 @@ nm_l3_config_data_merge(NML3ConfigData       *self,
         self->routed_dns_4 = TRUE;
     if (src->routed_dns_6)
         self->routed_dns_6 = TRUE;
+
+    if (self->clat_config == NM_SETTING_IP4_CONFIG_CLAT_NO) {
+        /* 'no' always loses to 'force' and 'auto' */
+        self->clat_config = src->clat_config;
+    } else if (src->clat_config == NM_SETTING_IP4_CONFIG_CLAT_FORCE) {
+        /* 'force' always takes precedence */
+        self->clat_config = src->clat_config;
+    }
+
+    if (!self->clat_state.enabled && src->clat_state.enabled) {
+        self->clat_state = src->clat_state;
+    }
+
+    if (src->pref64_valid) {
+        self->pref64_prefix = src->pref64_prefix;
+        self->pref64_plen   = src->pref64_plen;
+        self->pref64_valid  = src->pref64_valid;
+    }
 }
 
 NML3ConfigData *
diff --git a/src/core/nm-l3-config-data.h b/src/core/nm-l3-config-data.h
index 4102b6e1..b76e11f9 100644
--- a/src/core/nm-l3-config-data.h
+++ b/src/core/nm-l3-config-data.h
@@ -5,6 +5,7 @@
 
 #include "libnm-glib-aux/nm-dedup-multi.h"
 #include "nm-setting-connection.h"
+#include "nm-setting-ip4-config.h"
 #include "nm-setting-ip6-config.h"
 #include "libnm-platform/nm-platform.h"
 #include "libnm-platform/nmp-object.h"
@@ -225,6 +226,10 @@ nm_l3_config_data_equal(const NML3ConfigData *a, const NML3ConfigData *b)
 
 /*****************************************************************************/
 
+const NMPObject *nm_l3_config_data_get_direct_route_for_host(const NML3ConfigData *self,
+                                                             int                   addr_family,
+                                                             gconstpointer         host);
+
 const NMDedupMultiIdxType *nm_l3_config_data_lookup_index(const NML3ConfigData *self,
                                                           NMPObjectType         obj_type);
 
@@ -482,14 +487,49 @@ guint32 nm_l3_config_data_get_mtu(const NML3ConfigData *self);
 
 gboolean nm_l3_config_data_set_mtu(NML3ConfigData *self, guint32 mtu);
 
-guint32 nm_l3_config_data_get_ip6_mtu(const NML3ConfigData *self);
+guint32 nm_l3_config_data_get_ip6_mtu_static(const NML3ConfigData *self);
+
+gboolean nm_l3_config_data_set_ip6_mtu_static(NML3ConfigData *self, guint32 ip6_mtu);
 
-gboolean nm_l3_config_data_set_ip6_mtu(NML3ConfigData *self, guint32 ip6_mtu);
+guint32 nm_l3_config_data_get_ip6_mtu_ra(const NML3ConfigData *self);
+
+gboolean nm_l3_config_data_set_ip6_mtu_ra(NML3ConfigData *self, guint32 ip6_mtu);
 
 NMUtilsIPv6IfaceId nm_l3_config_data_get_ip6_token(const NML3ConfigData *self);
 
 gboolean nm_l3_config_data_set_ip6_token(NML3ConfigData *self, NMUtilsIPv6IfaceId ipv6_token);
 
+gboolean nm_l3_config_data_set_network_id(NML3ConfigData *self, const char *network_id);
+
+const char *nm_l3_config_data_get_network_id(const NML3ConfigData *self);
+
+gboolean nm_l3_config_data_set_clat_config(NML3ConfigData *self, NMSettingIp4ConfigClat val);
+
+NMSettingIp4ConfigClat nm_l3_config_data_get_clat_config(const NML3ConfigData *self);
+
+gboolean nm_l3_config_data_get_clat_state(const NML3ConfigData *self,
+                                          struct in6_addr      *out_ip6,
+                                          struct in6_addr      *out_pref64,
+                                          guint8               *out_pref64_plen,
+                                          in_addr_t            *out_ip4);
+
+void nm_l3_config_data_set_clat_state(NML3ConfigData        *self,
+                                      gboolean               enabled,
+                                      const struct in6_addr *ip6,
+                                      const struct in6_addr *pref64,
+                                      guint8                 pref64_plen,
+                                      in_addr_t              ip4);
+
+gboolean nm_l3_config_data_set_pref64_valid(NML3ConfigData *self, gboolean val);
+
+gboolean nm_l3_config_data_get_pref64_valid(const NML3ConfigData *self);
+
+gboolean nm_l3_config_data_get_pref64(const NML3ConfigData *self,
+                                      struct in6_addr      *out_prefix,
+                                      guint32              *out_plen);
+
+gboolean nm_l3_config_data_set_pref64(NML3ConfigData *self, struct in6_addr prefix, guint32 plen);
+
 NMMptcpFlags nm_l3_config_data_get_mptcp_flags(const NML3ConfigData *self);
 
 gboolean nm_l3_config_data_set_mptcp_flags(NML3ConfigData *self, NMMptcpFlags mptcp_flags);
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);
diff --git a/src/core/nm-manager.c b/src/core/nm-manager.c
index 87dde2c3..7099cb8a 100644
--- a/src/core/nm-manager.c
+++ b/src/core/nm-manager.c
@@ -11,7 +11,6 @@
 #include <fcntl.h>
 #include <limits.h>
 #include <stdlib.h>
-#include <sys/sendfile.h>
 #include <sys/stat.h>
 #include <sys/types.h>
 #include <unistd.h>
@@ -3585,34 +3584,6 @@ get_existing_connection(NMManager *self, NMDevice *device, gboolean *out_generat
 }
 
 static gboolean
-copy_lease(const char *src, const char *dst)
-{
-    nm_auto_close int src_fd = -1;
-    int               dst_fd;
-    ssize_t           res, size = SSIZE_MAX;
-
-    src_fd = open(src, O_RDONLY | O_CLOEXEC);
-    if (src_fd < 0)
-        return FALSE;
-
-    dst_fd = open(dst, O_CREAT | O_EXCL | O_CLOEXEC | O_WRONLY, 0644);
-    if (dst_fd < 0)
-        return FALSE;
-
-    while ((res = sendfile(dst_fd, src_fd, NULL, size)) > 0)
-        size -= res;
-
-    nm_close(dst_fd);
-
-    if (res != 0) {
-        unlink(dst);
-        return FALSE;
-    }
-
-    return TRUE;
-}
-
-static gboolean
 recheck_assume_connection(NMManager *self, NMDevice *device)
 {
     NMSettingsConnection *sett_conn;
@@ -3652,18 +3623,9 @@ recheck_assume_connection(NMManager *self, NMDevice *device)
     if (state == NM_DEVICE_STATE_UNMANAGED) {
         gs_free char *initramfs_lease =
             g_strdup_printf(RUNSTATEDIR "/initramfs/net.%s.lease", nm_device_get_iface(device));
-        gs_free char *connection_lease = g_strdup_printf(NMRUNDIR "/dhclient-%s-%s.lease",
-                                                         nm_settings_connection_get_uuid(sett_conn),
-                                                         nm_device_get_iface(device));
 
-        if (copy_lease(initramfs_lease, connection_lease)) {
+        if (g_file_test(initramfs_lease, G_FILE_TEST_EXISTS)) {
             unlink(initramfs_lease);
-            /*
-             * We've managed to steal the lease used by initramfs before it
-             * killed off the dhclient. We need to take ownership of the configured
-             * connection and act like the device was configured by us.
-             * Otherwise, the address would just expire.
-             */
             _LOG2I(LOGD_DEVICE, device, "assume: taking over an initramfs-configured connection");
             activation_type_assume = TRUE;
 
@@ -3885,10 +3847,19 @@ _get_best_connectivity(NMManager *self, int addr_family)
         if (NM_IS_DEVICE_LOOPBACK(dev))
             continue;
 
-        r = nm_device_get_best_default_route(dev, addr_family);
-        if (r)
+        r     = nm_device_get_best_default_route(dev, addr_family);
+        state = nm_device_get_connectivity_state(dev, addr_family);
+
+        if (r) {
+            /* If a default-route device is FULL, that is the global state: its
+             * route carries no penalty, so it outranks any non-FULL device (which
+             * gets +20000). Decide on the state directly, not the metric, which is
+             * stale here because the penalty commits to the route asynchronously
+             * after this recompute. */
+            if (nm_connectivity_state_cmp(state, NM_CONNECTIVITY_FULL) >= 0)
+                return NM_CONNECTIVITY_FULL;
             metric = NMP_OBJECT_CAST_IP_ROUTE(r)->metric;
-        else {
+        } else {
             /* if all devices have no default-route, we still include the best
              * of all connectivity state of all the devices. */
             metric = G_MAXINT64;
@@ -3896,11 +3867,10 @@ _get_best_connectivity(NMManager *self, int addr_family)
 
         if (metric > best_metric) {
             /* we already have a default route with better metric. The connectivity state
-             * of this device is irreleavnt. */
+             * of this device is irrelevant. */
             continue;
         }
 
-        state = nm_device_get_connectivity_state(dev, addr_family);
         if (metric < best_metric) {
             /* this device has a better default route. It wins. */
             best_metric = metric;
@@ -8146,6 +8116,27 @@ nm_manager_start(NMManager *self, GError **error)
     return TRUE;
 }
 
+static int
+compare_device_remove_order(const CList *a, const CList *b, const void *user_data)
+{
+    NMDevice *dev_a = c_list_entry(a, NMDevice, devices_lst);
+    NMDevice *dev_b = c_list_entry(b, NMDevice, devices_lst);
+
+    gboolean a_has_dhcp =
+        nm_device_get_dhcp_config(dev_a, AF_INET) || nm_device_get_dhcp_config(dev_a, AF_INET6);
+    gboolean b_has_dhcp =
+        nm_device_get_dhcp_config(dev_b, AF_INET) || nm_device_get_dhcp_config(dev_b, AF_INET6);
+    gboolean a_is_software = nm_device_is_software(dev_a);
+    gboolean b_is_software = nm_device_is_software(dev_b);
+
+    /* priority: software AND dhcp first, then dhcp only
+     * then everything else,*/
+    int a_score = a_has_dhcp ? (a_is_software ? 2 : 1) : 0;
+    int b_score = b_has_dhcp ? (b_is_software ? 2 : 1) : 0;
+
+    return b_score - a_score;
+}
+
 void
 nm_manager_stop(NMManager *self)
 {
@@ -8167,6 +8158,12 @@ nm_manager_stop(NMManager *self)
 
     nm_dbus_manager_stop(nm_dbus_object_get_manager(NM_DBUS_OBJECT(self)));
 
+    /* When OVS internal interface or linux bridge holds DHCP, if we delete its
+     * physical interface first, then we cannot send out DHCP release request
+     * anymore. To fix that, we need to remove/deactivate software interfaces that
+     * holds DHCP config first.
+     */
+    c_list_sort(&priv->devices_lst_head, compare_device_remove_order, NULL);
     while ((device = c_list_first_entry(&priv->devices_lst_head, NMDevice, devices_lst)))
         remove_device(self, device, TRUE);
 
diff --git a/src/core/nm-netns.c b/src/core/nm-netns.c
index 0e8b15a7..65142f4f 100644
--- a/src/core/nm-netns.c
+++ b/src/core/nm-netns.c
@@ -576,8 +576,8 @@ notify_watcher:
 typedef struct {
     const char *name;
     guint32     start_addr; /* host byte order */
-    guint       prefix_len;
-    guint       num_addrs;
+    guint       range_plen;
+    guint       addr_plen;
     gboolean    allow_reuse;
 } IPReservationTypeDesc;
 
@@ -585,11 +585,19 @@ static const IPReservationTypeDesc ip_reservation_types[_NM_NETNS_IP_RESERVATION
     [NM_NETNS_IP_RESERVATION_TYPE_SHARED4] =
         {
             .name        = "shared-ip4",
-            .start_addr  = 0x0a2a0001, /* 10.42.0.1 */
-            .prefix_len  = 24,
-            .num_addrs   = 256,
+            .start_addr  = 0x0a2a0001, /* 10.42.{0-255}.1/24 */
+            .range_plen  = 16,
+            .addr_plen   = 24,
             .allow_reuse = TRUE,
         },
+    [NM_NETNS_IP_RESERVATION_TYPE_CLAT] =
+        {
+            .name        = "clat",
+            .start_addr  = 0xc0000005, /* 192.0.0.{5-7,0-4}/32 */
+            .range_plen  = 29,
+            .addr_plen   = 32,
+            .allow_reuse = FALSE,
+        },
 };
 
 NMNetnsIPReservation *
@@ -615,13 +623,23 @@ nm_netns_ip_reservation_get(NMNetns *self, NMNetnsIPReservationType type)
         g_object_ref(self);
     } else {
         guint32 count;
+        guint32 base_network;
+        guint32 host_mask;
+        guint32 increment;
 
         nm_assert(g_hash_table_size(*table) > 0);
-        nm_assert(desc->prefix_len > 0 && desc->prefix_len <= 32);
+        nm_assert(desc->range_plen < 32);
+        nm_assert(desc->addr_plen > 0 && desc->addr_plen <= 32);
+        nm_assert(desc->addr_plen > desc->range_plen);
+
+        base_network = desc->start_addr & ~(0xFFFFFFFFu >> desc->range_plen);
+        host_mask    = 0xFFFFFFFFu >> desc->range_plen;
+        increment    = 1 << (32 - desc->addr_plen);
 
         count = 0u;
         for (;;) {
-            addr = htonl(desc->start_addr + (count << (32 - desc->prefix_len)));
+            addr = htonl(base_network
+                         + ((base_network + (desc->start_addr + count * increment)) & host_mask));
 
             res = g_hash_table_lookup(*table, &addr);
             if (!res)
@@ -629,7 +647,7 @@ nm_netns_ip_reservation_get(NMNetns *self, NMNetnsIPReservationType type)
 
             count++;
 
-            if (count >= desc->num_addrs) {
+            if (count >= 1 << (desc->addr_plen - desc->range_plen)) {
                 if (!desc->allow_reuse) {
                     _LOGE("%s: ran out of IP addresses", desc->name);
                     return NULL;
@@ -639,12 +657,12 @@ nm_netns_ip_reservation_get(NMNetns *self, NMNetnsIPReservationType type)
                     _LOGE("%s: ran out of IP addresses. Reuse %s/%u",
                           desc->name,
                           nm_inet4_ntop(res->addr, buf),
-                          desc->prefix_len);
+                          desc->addr_plen);
                 } else {
                     _LOGD("%s: reserved IP address %s/%u (duplicate)",
                           desc->name,
                           nm_inet4_ntop(res->addr, buf),
-                          desc->prefix_len);
+                          desc->addr_plen);
                 }
                 res->_ref_count++;
                 return res;
@@ -665,7 +683,7 @@ nm_netns_ip_reservation_get(NMNetns *self, NMNetnsIPReservationType type)
     _LOGD("%s: reserved IP address %s/%u",
           desc->name,
           nm_inet4_ntop(res->addr, buf),
-          desc->prefix_len);
+          desc->addr_plen);
     return res;
 }
 
@@ -697,7 +715,7 @@ nm_netns_ip_reservation_release(NMNetnsIPReservation *res)
         _LOGD("%s: release IP address reservation %s/%u (%d more references held)",
               desc->name,
               nm_inet4_ntop(res->addr, buf),
-              desc->prefix_len,
+              desc->addr_plen,
               res->_ref_count);
         return;
     }
@@ -708,7 +726,7 @@ nm_netns_ip_reservation_release(NMNetnsIPReservation *res)
     _LOGD("%s: release IP address reservation %s/%u",
           desc->name,
           nm_inet4_ntop(res->addr, buf),
-          desc->prefix_len);
+          desc->addr_plen);
 
     if (g_hash_table_size(*table) == 0) {
         nm_clear_pointer(table, g_hash_table_unref);
diff --git a/src/core/nm-netns.h b/src/core/nm-netns.h
index 5ddb852a..e32d5680 100644
--- a/src/core/nm-netns.h
+++ b/src/core/nm-netns.h
@@ -43,6 +43,7 @@ NML3Cfg *nm_netns_l3cfg_acquire(NMNetns *netns, int ifindex);
 
 typedef enum {
     NM_NETNS_IP_RESERVATION_TYPE_SHARED4,
+    NM_NETNS_IP_RESERVATION_TYPE_CLAT,
 
     _NM_NETNS_IP_RESERVATION_TYPE_NUM,
 } NMNetnsIPReservationType;
diff --git a/src/core/nm-pacrunner-manager.c b/src/core/nm-pacrunner-manager.c
index 12c3c9a1..9f4a294b 100644
--- a/src/core/nm-pacrunner-manager.c
+++ b/src/core/nm-pacrunner-manager.c
@@ -122,43 +122,6 @@ NM_AUTO_DEFINE_FCN0(NMPacrunnerConfId *, _nm_auto_unref_conf_id, conf_id_unref);
 
 /*****************************************************************************/
 
-static void
-get_ip_domains(GPtrArray *domains, const NML3ConfigData *l3cd, int addr_family)
-{
-    NMDedupMultiIter           ipconf_iter;
-    char                      *cidr;
-    guint                      num;
-    guint                      i;
-    char                       sbuf[NM_INET_ADDRSTRLEN];
-    const NMPlatformIPAddress *address;
-    const NMPlatformIPRoute   *route;
-    const char *const         *strv;
-
-    strv = nm_l3_config_data_get_searches(l3cd, addr_family, &num);
-    for (i = 0; i < num; i++)
-        g_ptr_array_add(domains, g_strdup(strv[i]));
-
-    strv = nm_l3_config_data_get_domains(l3cd, addr_family, &num);
-    for (i = 0; i < num; i++)
-        g_ptr_array_add(domains, g_strdup(strv[i]));
-
-    nm_l3_config_data_iter_ip_address_for_each (&ipconf_iter, l3cd, addr_family, &address) {
-        cidr = g_strdup_printf("%s/%u",
-                               nm_inet_ntop(addr_family, address->address_ptr, sbuf),
-                               address->plen);
-        g_ptr_array_add(domains, cidr);
-    }
-
-    nm_l3_config_data_iter_ip_route_for_each (&ipconf_iter, l3cd, addr_family, &route) {
-        if (NM_PLATFORM_IP_ROUTE_IS_DEFAULT(route))
-            continue;
-        cidr = g_strdup_printf("%s/%u",
-                               nm_inet_ntop(addr_family, route->network_ptr, sbuf),
-                               route->plen);
-        g_ptr_array_add(domains, cidr);
-    }
-}
-
 static GVariant *
 _make_request_create_proxy_configuration(const char *iface, const NML3ConfigData *l3cd)
 {
@@ -198,23 +161,6 @@ _make_request_create_proxy_configuration(const char *iface, const NML3ConfigData
         "BrowserOnly",
         g_variant_new_boolean(l3cd ? !!nm_l3_config_data_get_proxy_browser_only(l3cd) : FALSE));
 
-    if (l3cd) {
-        gs_unref_ptrarray GPtrArray *domains = NULL;
-
-        domains = g_ptr_array_new_with_free_func(g_free);
-
-        get_ip_domains(domains, l3cd, AF_INET);
-        get_ip_domains(domains, l3cd, AF_INET6);
-
-        if (domains->len > 0) {
-            g_variant_builder_add(
-                &builder,
-                "{sv}",
-                "Domains",
-                g_variant_new_strv((const char *const *) domains->pdata, domains->len));
-        }
-    }
-
     return g_variant_new("(a{sv})", &builder);
 }
 
diff --git a/src/core/nm-policy.c b/src/core/nm-policy.c
index f7be1a9f..48c004bf 100644
--- a/src/core/nm-policy.c
+++ b/src/core/nm-policy.c
@@ -204,10 +204,10 @@ expire_ip6_delegations(NMPolicy *self)
     IP6PrefixDelegation *delegation = NULL;
     guint                i;
 
-    for (i = 0; i < priv->ip6_prefix_delegations->len; i++) {
-        delegation = &nm_g_array_index(priv->ip6_prefix_delegations, IP6PrefixDelegation, i);
+    for (i = priv->ip6_prefix_delegations->len; i > 0; i--) {
+        delegation = &nm_g_array_index(priv->ip6_prefix_delegations, IP6PrefixDelegation, i - 1);
         if (delegation->prefix.timestamp + delegation->prefix.lifetime < now)
-            g_array_remove_index_fast(priv->ip6_prefix_delegations, i);
+            g_array_remove_index(priv->ip6_prefix_delegations, i - 1);
     }
 }
 
@@ -253,7 +253,7 @@ ip6_subnet_from_delegation(IP6PrefixDelegation *delegation, NMDevice *device)
     }
 
     /* Check for out-of-prefixes condition */
-    num_subnets = 1 << (64 - delegation->prefix.plen);
+    num_subnets = (guint64) 1 << (64 - delegation->prefix.plen);
     if (nm_g_hash_table_size(delegation->map_subnet_id_to_ifindex) >= num_subnets) {
         _LOGD(LOGD_IP6,
               "ipv6-pd: no more prefixes in %s/%u",
@@ -378,10 +378,10 @@ ip6_remove_device_prefix_delegations(NMPolicy *self, NMDevice *device)
     IP6PrefixDelegation *delegation = NULL;
     guint                i;
 
-    for (i = 0; i < priv->ip6_prefix_delegations->len; i++) {
-        delegation = &nm_g_array_index(priv->ip6_prefix_delegations, IP6PrefixDelegation, i);
+    for (i = priv->ip6_prefix_delegations->len; i > 0; i--) {
+        delegation = &nm_g_array_index(priv->ip6_prefix_delegations, IP6PrefixDelegation, i - 1);
         if (delegation->device == device)
-            g_array_remove_index_fast(priv->ip6_prefix_delegations, i);
+            g_array_remove_index(priv->ip6_prefix_delegations, i - 1);
     }
 }
 
diff --git a/src/core/ppp/nm-pppd-plugin.c b/src/core/ppp/nm-pppd-plugin.c
index c8a866ec..0bf58a0c 100644
--- a/src/core/ppp/nm-pppd-plugin.c
+++ b/src/core/ppp/nm-pppd-plugin.c
@@ -198,7 +198,7 @@ nm_ip_up(void *data, int arg)
         g_variant_builder_add(&builder,
                               "{sv}",
                               NM_PPP_IP4_CONFIG_GATEWAY,
-                              g_variant_new_uint32(peer_opts.ouraddr));
+                              g_variant_new_uint32(peer_opts.hisaddr));
     }
 
     g_variant_builder_add(&builder, "{sv}", NM_PPP_IP4_CONFIG_PREFIX, g_variant_new_uint32(32));
diff --git a/src/core/settings/nm-settings-connection.c b/src/core/settings/nm-settings-connection.c
index 7ed3712b..c8f5f290 100644
--- a/src/core/settings/nm-settings-connection.c
+++ b/src/core/settings/nm-settings-connection.c
@@ -1866,6 +1866,8 @@ impl_settings_connection_update2(NMDBusObject                      *obj,
 
     g_variant_iter_init(&iter, args);
     while (g_variant_iter_next(&iter, "{&sv}", &args_name, &args_value)) {
+        gs_unref_variant GVariant *args_value_unref = args_value;
+
         if (plugin_name == NULL && nm_streq(args_name, "plugin")
             && g_variant_is_of_type(args_value, G_VARIANT_TYPE_STRING)) {
             plugin_name = g_variant_dup_string(args_value, NULL);
diff --git a/src/core/settings/nm-settings.c b/src/core/settings/nm-settings.c
index 702c53d5..3fe9c76a 100644
--- a/src/core/settings/nm-settings.c
+++ b/src/core/settings/nm-settings.c
@@ -2934,6 +2934,8 @@ impl_settings_add_connection2(NMDBusObject                      *obj,
 
     g_variant_iter_init(&iter, args);
     while (g_variant_iter_next(&iter, "{&sv}", &args_name, &args_value)) {
+        gs_unref_variant GVariant *args_value_unref = args_value;
+
         if (plugin == NULL && nm_streq(args_name, "plugin")
             && g_variant_is_of_type(args_value, G_VARIANT_TYPE_STRING)) {
             plugin = g_variant_dup_string(args_value, NULL);
diff --git a/src/core/settings/plugins/ifcfg-rh/nms-ifcfg-rh-reader.c b/src/core/settings/plugins/ifcfg-rh/nms-ifcfg-rh-reader.c
index 728dccac..6312154c 100644
--- a/src/core/settings/plugins/ifcfg-rh/nms-ifcfg-rh-reader.c
+++ b/src/core/settings/plugins/ifcfg-rh/nms-ifcfg-rh-reader.c
@@ -4401,7 +4401,7 @@ make_wireless_setting(shvarFile *ifcfg, GError **error)
     NMSettingWireless         *s_wireless;
     const char                *cvalue;
     char                      *value = NULL;
-    gint64                     chan  = 0;
+    guint64                    chan  = 0;
     NMSettingMacRandomization  mac_randomization;
     NMSettingWirelessPowersave powersave = NM_SETTING_WIRELESS_POWERSAVE_DEFAULT;
     NMTernary                  ternary;
@@ -4502,7 +4502,7 @@ make_wireless_setting(shvarFile *ifcfg, GError **error)
 
     value = svGetValueStr_cp(ifcfg, "CHANNEL");
     if (value) {
-        chan = _nm_utils_ascii_str_to_int64(value, 10, 1, 196, 0);
+        chan = _nm_utils_ascii_str_to_int64(value, 10, 1, _NM_WIFI_CHANNEL_MAX, 0);
         if (chan == 0) {
             g_set_error(error,
                         NM_SETTINGS_ERROR,
@@ -4518,19 +4518,18 @@ make_wireless_setting(shvarFile *ifcfg, GError **error)
 
     value = svGetValueStr_cp(ifcfg, "BAND");
     if (value) {
-        if (!strcmp(value, "a")) {
-            if (chan && chan <= 14) {
-                g_set_error(error,
-                            NM_SETTINGS_ERROR,
-                            NM_SETTINGS_ERROR_INVALID_CONNECTION,
-                            "Band '%s' invalid for channel %u",
-                            value,
-                            (guint32) chan);
-                g_free(value);
-                goto error;
-            }
-        } else if (!strcmp(value, "bg")) {
-            if (chan && chan > 14) {
+        if (!NM_IN_STRSET(value, "a", "bg", "6GHz")) {
+            g_set_error(error,
+                        NM_SETTINGS_ERROR,
+                        NM_SETTINGS_ERROR_INVALID_CONNECTION,
+                        "Band '%s' invalid",
+                        value);
+            g_free(value);
+            goto error;
+        }
+
+        if (chan) {
+            if (!nm_utils_wifi_is_channel_valid(chan, value)) {
                 g_set_error(error,
                             NM_SETTINGS_ERROR,
                             NM_SETTINGS_ERROR_INVALID_CONNECTION,
@@ -4540,22 +4539,26 @@ make_wireless_setting(shvarFile *ifcfg, GError **error)
                 g_free(value);
                 goto error;
             }
-        } else {
+        }
+
+        g_object_set(s_wireless, NM_SETTING_WIRELESS_BAND, value, NULL);
+        g_free(value);
+    } else if (chan > 0) {
+        if (chan > _NM_WIFI_CHANNEL_MAX_5GHZ) {
             g_set_error(error,
                         NM_SETTINGS_ERROR,
                         NM_SETTINGS_ERROR_INVALID_CONNECTION,
-                        "Invalid wireless band '%s'",
-                        value);
+                        "Setting channel without band is ambiguous and deprecated. Not supported "
+                        "for 6GHz.");
             g_free(value);
             goto error;
-        }
-        g_object_set(s_wireless, NM_SETTING_WIRELESS_BAND, value, NULL);
-        g_free(value);
-    } else if (chan > 0) {
-        if (chan > 14)
+        } else if (chan > _NM_WIFI_CHANNEL_MAX_2GHZ) {
+            PARSE_WARNING(
+                "Setting channel without band is ambiguous and deprecated. Assuming band 'a'.");
             g_object_set(s_wireless, NM_SETTING_WIRELESS_BAND, "a", NULL);
-        else
+        } else {
             g_object_set(s_wireless, NM_SETTING_WIRELESS_BAND, "bg", NULL);
+        }
     }
 
     value = svGetValueStr_cp(ifcfg, "MTU");
diff --git a/src/core/settings/plugins/ifcfg-rh/nms-ifcfg-rh-writer.c b/src/core/settings/plugins/ifcfg-rh/nms-ifcfg-rh-writer.c
index 21908090..dc60fdf1 100644
--- a/src/core/settings/plugins/ifcfg-rh/nms-ifcfg-rh-writer.c
+++ b/src/core/settings/plugins/ifcfg-rh/nms-ifcfg-rh-writer.c
@@ -849,7 +849,7 @@ write_wireless_setting(NMConnection *connection,
     GBytes            *ssid;
     const guint8      *ssid_data;
     gsize              ssid_len;
-    const char        *mode, *bssid;
+    const char        *mode, *bssid, *band;
     const char        *device_mac, *cloned_mac;
     guint32            mtu, chan, i;
     gboolean           adhoc = FALSE, hex_ssid = FALSE;
@@ -968,9 +968,11 @@ write_wireless_setting(NMConnection *connection,
     chan = nm_setting_wireless_get_channel(s_wireless);
     if (chan) {
         svSetValueInt64(ifcfg, "CHANNEL", chan);
-    } else {
-        /* Band only set if channel is not, since channel implies band */
-        svSetValueStr(ifcfg, "BAND", nm_setting_wireless_get_band(s_wireless));
+    }
+
+    band = nm_setting_wireless_get_band(s_wireless);
+    if (band) {
+        svSetValueStr(ifcfg, "BAND", band);
     }
 
     bssid = nm_setting_wireless_get_bssid(s_wireless);
@@ -3598,6 +3600,7 @@ do_write_construct(NMConnection                   *connection,
     } else
         route_ignore = FALSE;
 
+    /* Unsupported properties */
     if ((s_ip4 = nm_connection_get_setting_ip4_config(connection))) {
         if (nm_setting_ip_config_get_dhcp_dscp(s_ip4)) {
             set_error_unsupported(error,
@@ -3616,6 +3619,14 @@ do_write_construct(NMConnection                   *connection,
                                   FALSE);
             return FALSE;
         }
+        if (nm_setting_ip4_config_get_clat(NM_SETTING_IP4_CONFIG(s_ip4))
+            != NM_SETTING_IP4_CONFIG_CLAT_DEFAULT) {
+            set_error_unsupported(error,
+                                  connection,
+                                  NM_SETTING_IP4_CONFIG_SETTING_NAME "." NM_SETTING_IP4_CONFIG_CLAT,
+                                  FALSE);
+            return FALSE;
+        }
     }
 
     write_ip4_setting(connection,
diff --git a/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-Test_Write_WiFi_AP_Mode.cexpected b/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-Test_Write_WiFi_AP_Mode.cexpected
index caeaaff8..8199a9b4 100644
--- a/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-Test_Write_WiFi_AP_Mode.cexpected
+++ b/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-Test_Write_WiFi_AP_Mode.cexpected
@@ -1,6 +1,7 @@
 ESSID=MySSID
 MODE=Ap
-CHANNEL=196
+CHANNEL=52
+BAND=a
 MAC_ADDRESS_RANDOMIZATION=default
 AP_ISOLATION=yes
 TYPE=Wireless
diff --git a/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-Test_Write_WiFi_Band_6ghz.cexpected b/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-Test_Write_WiFi_Band_6ghz.cexpected
new file mode 100644
index 00000000..d8ffe48e
--- /dev/null
+++ b/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-Test_Write_WiFi_Band_6ghz.cexpected
@@ -0,0 +1,18 @@
+ESSID="Test SSID"
+MODE=Managed
+BAND=6GHz
+MAC_ADDRESS_RANDOMIZATION=default
+TYPE=Wireless
+PROXY_METHOD=none
+BROWSER_ONLY=no
+BOOTPROTO=dhcp
+DEFROUTE=yes
+IPV4_FAILURE_FATAL=no
+IPV6INIT=yes
+IPV6_AUTOCONF=yes
+IPV6_DEFROUTE=yes
+IPV6_FAILURE_FATAL=no
+IPV6_ADDR_GEN_MODE=default
+NAME="Test Write Wi-Fi Band 6GHz"
+UUID=${UUID}
+ONBOOT=yes
diff --git a/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-Test_Write_WiFi_Band_A.cexpected b/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-Test_Write_WiFi_Band_a.cexpected
index 7e3d4f02..90570ea3 100644
--- a/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-Test_Write_WiFi_Band_A.cexpected
+++ b/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-Test_Write_WiFi_Band_a.cexpected
@@ -13,6 +13,6 @@ IPV6_AUTOCONF=yes
 IPV6_DEFROUTE=yes
 IPV6_FAILURE_FATAL=no
 IPV6_ADDR_GEN_MODE=default
-NAME="Test Write Wi-Fi Band A"
+NAME="Test Write Wi-Fi Band A - 5GHz"
 UUID=${UUID}
 ONBOOT=yes
diff --git a/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-test-wifi-band-6ghz b/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-test-wifi-band-6ghz
new file mode 100644
index 00000000..803ca19b
--- /dev/null
+++ b/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-test-wifi-band-6ghz
@@ -0,0 +1,13 @@
+TYPE=Wireless
+DEVICE=eth2
+HWADDR=00:16:41:11:22:33
+NM_CONTROLLED=yes
+BOOTPROTO=dhcp
+ESSID=blahblah
+BAND=6GHz
+MODE=Managed
+RATE=auto
+ONBOOT=yes
+USERCTL=yes
+PEERDNS=yes
+IPV6INIT=no
diff --git a/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-test-wifi-band-6ghz-channel-mismatch b/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-test-wifi-band-6ghz-channel-mismatch
new file mode 100644
index 00000000..26fc29fe
--- /dev/null
+++ b/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-test-wifi-band-6ghz-channel-mismatch
@@ -0,0 +1,9 @@
+TYPE=Wireless
+DEVICE=eth2
+HWADDR=00:16:41:11:22:33
+BOOTPROTO=dhcp
+ESSID=blahblah
+CHANNEL=14
+BAND=6GHz
+MODE=Managed
+
diff --git a/src/core/settings/plugins/ifcfg-rh/tests/test-ifcfg-rh.c b/src/core/settings/plugins/ifcfg-rh/tests/test-ifcfg-rh.c
index 2f3035ef..f5f80ed8 100644
--- a/src/core/settings/plugins/ifcfg-rh/tests/test-ifcfg-rh.c
+++ b/src/core/settings/plugins/ifcfg-rh/tests/test-ifcfg-rh.c
@@ -3991,7 +3991,7 @@ test_write_wifi_band_a(void)
     s_con = _nm_connection_new_setting(connection, NM_TYPE_SETTING_CONNECTION);
     g_object_set(s_con,
                  NM_SETTING_CONNECTION_ID,
-                 "Test Write Wi-Fi Band A",
+                 "Test Write Wi-Fi Band A - 5GHz",
                  NM_SETTING_CONNECTION_UUID,
                  nm_uuid_generate_random_str_a(),
                  NM_SETTING_CONNECTION_TYPE,
@@ -4012,7 +4012,7 @@ test_write_wifi_band_a(void)
 
     _writer_new_connec_exp(connection,
                            TEST_SCRATCH_DIR,
-                           TEST_IFCFG_DIR "/ifcfg-Test_Write_WiFi_Band_A.cexpected",
+                           TEST_IFCFG_DIR "/ifcfg-Test_Write_WiFi_Band_a.cexpected",
                            &testfile);
 
     f = _svOpenFile(testfile);
@@ -4025,6 +4025,77 @@ test_write_wifi_band_a(void)
 }
 
 static void
+test_read_wifi_band_6ghz(void)
+{
+    gs_unref_object NMConnection *connection = NULL;
+    NMSettingConnection          *s_con;
+    NMSettingWireless            *s_wifi;
+
+    connection = _connection_from_file(TEST_IFCFG_DIR "/ifcfg-test-wifi-band-6ghz",
+                                       NULL,
+                                       TYPE_WIRELESS,
+                                       NULL);
+
+    s_con = nmtst_connection_assert_setting(connection, NM_TYPE_SETTING_CONNECTION);
+    g_assert_cmpstr(nm_setting_connection_get_connection_type(s_con),
+                    ==,
+                    NM_SETTING_WIRELESS_SETTING_NAME);
+
+    s_wifi = nmtst_connection_assert_setting(connection, NM_TYPE_SETTING_WIRELESS);
+    g_assert_cmpstr(nm_setting_wireless_get_band(s_wifi), ==, "6GHz");
+}
+
+static void
+test_write_wifi_band_6ghz(void)
+{
+    nmtst_auto_unlinkfile char   *testfile   = NULL;
+    gs_unref_object NMConnection *connection = NULL;
+    gs_unref_object NMConnection *reread     = NULL;
+    NMSettingConnection          *s_con;
+    NMSettingWireless            *s_wifi;
+    shvarFile                    *f;
+    gs_unref_bytes GBytes        *ssid =
+        nmtst_gbytes_from_arr(0x54, 0x65, 0x73, 0x74, 0x20, 0x53, 0x53, 0x49, 0x44);
+
+    connection = nm_simple_connection_new();
+
+    s_con = _nm_connection_new_setting(connection, NM_TYPE_SETTING_CONNECTION);
+    g_object_set(s_con,
+                 NM_SETTING_CONNECTION_ID,
+                 "Test Write Wi-Fi Band 6GHz",
+                 NM_SETTING_CONNECTION_UUID,
+                 nm_uuid_generate_random_str_a(),
+                 NM_SETTING_CONNECTION_TYPE,
+                 NM_SETTING_WIRELESS_SETTING_NAME,
+                 NULL);
+
+    s_wifi = _nm_connection_new_setting(connection, NM_TYPE_SETTING_WIRELESS);
+    g_object_set(s_wifi,
+                 NM_SETTING_WIRELESS_SSID,
+                 ssid,
+                 NM_SETTING_WIRELESS_MODE,
+                 "infrastructure",
+                 NM_SETTING_WIRELESS_BAND,
+                 "6GHz",
+                 NULL);
+
+    nmtst_assert_connection_verifies(connection);
+
+    _writer_new_connec_exp(connection,
+                           TEST_SCRATCH_DIR,
+                           TEST_IFCFG_DIR "/ifcfg-Test_Write_WiFi_Band_6ghz.cexpected",
+                           &testfile);
+
+    f = _svOpenFile(testfile);
+    _svGetValue_check(f, "BAND", "6GHz");
+    svCloseFile(f);
+
+    reread = _connection_from_file(testfile, NULL, TYPE_WIRELESS, NULL);
+
+    nmtst_assert_connection_equals(connection, TRUE, reread, FALSE);
+}
+
+static void
 test_write_wifi_ap_mode(void)
 {
     nmtst_auto_unlinkfile char   *testfile   = NULL;
@@ -4055,7 +4126,7 @@ test_write_wifi_ap_mode(void)
                  NM_SETTING_WIRELESS_BAND,
                  "a",
                  NM_SETTING_WIRELESS_CHANNEL,
-                 (guint) 196,
+                 (guint) 52,
                  NM_SETTING_WIRELESS_AP_ISOLATION,
                  NM_TERNARY_TRUE,
                  NULL);
@@ -4073,6 +4144,18 @@ test_write_wifi_ap_mode(void)
 }
 
 static void
+test_read_wifi_band_6ghz_channel_mismatch(void)
+{
+    gs_free_error GError *error = NULL;
+
+    _connection_from_file_fail(TEST_IFCFG_DIR "/ifcfg-test-wifi-band-6ghz-channel-mismatch",
+                               NULL,
+                               TYPE_WIRELESS,
+                               &error);
+    g_assert_error(error, NM_SETTINGS_ERROR, NM_SETTINGS_ERROR_INVALID_CONNECTION);
+}
+
+static void
 test_read_wifi_band_a_channel_mismatch(void)
 {
     gs_free_error GError *error = NULL;
@@ -10680,6 +10763,9 @@ main(int argc, char **argv)
                     test_read_wifi_band_a_channel_mismatch);
     g_test_add_func(TPATH "wifi/read-band-bg-channel-mismatch",
                     test_read_wifi_band_bg_channel_mismatch);
+    g_test_add_func(TPATH "wifi/read-band-6ghz", test_read_wifi_band_6ghz);
+    g_test_add_func(TPATH "wifi/read-band-6ghz-channel-mismatch",
+                    test_read_wifi_band_6ghz_channel_mismatch);
     g_test_add_func(TPATH "wifi/read-hidden", test_read_wifi_hidden);
 
     nmtst_add_test_func(TPATH "wifi/read-mac-random-always",
@@ -10852,6 +10938,7 @@ main(int argc, char **argv)
                     test_write_wifi_wpa_then_wep_with_perms);
     g_test_add_func(TPATH "wifi/write-hidden", test_write_wifi_hidden);
     g_test_add_func(TPATH "wifi/write-band-a", test_write_wifi_band_a);
+    g_test_add_func(TPATH "wifi/write-band-6ghz", test_write_wifi_band_6ghz);
     g_test_add_func(TPATH "wifi/write-ap-mode", test_write_wifi_ap_mode);
 
     g_test_add_func(TPATH "s390/read-qeth-static", test_read_wired_qeth_static);
diff --git a/src/core/settings/plugins/keyfile/nms-keyfile-writer.c b/src/core/settings/plugins/keyfile/nms-keyfile-writer.c
index c7c88260..962f3d15 100644
--- a/src/core/settings/plugins/keyfile/nms-keyfile-writer.c
+++ b/src/core/settings/plugins/keyfile/nms-keyfile-writer.c
@@ -143,7 +143,9 @@ cert_writer(NMConnection                     *connection,
                                             vtable->setting_key,
                                             strrchr(new_path, '/') + 1);
         } else {
-            nm_log_warn(LOGD_SETTINGS,
+            g_set_error(error,
+                        NM_SETTINGS_ERROR,
+                        NM_SETTINGS_ERROR_FAILED,
                         "keyfile: %s.%s: failed to write certificate to file %s: %s",
                         setting_name,
                         vtable->setting_key,
diff --git a/src/core/supplicant/nm-supplicant-config.c b/src/core/supplicant/nm-supplicant-config.c
index 233afe48..a0ffb483 100644
--- a/src/core/supplicant/nm-supplicant-config.c
+++ b/src/core/supplicant/nm-supplicant-config.c
@@ -373,14 +373,24 @@ nm_supplicant_config_get_blobs(NMSupplicantConfig *self)
 }
 
 static const char *
-wifi_freqs_to_string(gboolean bg_band)
+wifi_freqs_to_string(const char *band)
 {
     static const char *str_2ghz = NULL;
     static const char *str_5ghz = NULL;
+    static const char *str_6ghz = NULL;
     const char       **f_p;
     const char        *f;
 
-    f_p = bg_band ? &str_2ghz : &str_5ghz;
+    if (nm_streq0(band, "a"))
+        f_p = &str_5ghz;
+    else if (nm_streq0(band, "bg"))
+        f_p = &str_2ghz;
+    else if (nm_streq0(band, "6GHz"))
+        f_p = &str_6ghz;
+    else {
+        nm_assert_not_reached();
+        return NULL;
+    }
 
 again:
     f = g_atomic_pointer_get(f_p);
@@ -390,7 +400,13 @@ again:
         const guint             *freqs;
         int                      i;
 
-        freqs = bg_band ? nm_utils_wifi_2ghz_freqs() : nm_utils_wifi_5ghz_freqs();
+        if (f_p == &str_2ghz)
+            freqs = nm_utils_wifi_2ghz_freqs();
+        else if (f_p == &str_5ghz)
+            freqs = nm_utils_wifi_5ghz_freqs();
+        else
+            freqs = nm_utils_wifi_6ghz_freqs();
+
         for (i = 0; freqs[i]; i++) {
             if (i > 0)
                 nm_str_buf_append_c(&strbuf, ' ');
@@ -533,38 +549,47 @@ get_ap_params(guint                         freq,
         guint channel;
         guint center_channel = 0;
 
-        if (freq < 5000) {
-            /* the setting is not valid */
-            nm_assert_not_reached();
-            return;
-        }
-
         /* Determine the center channel according to the table at
          * https://en.wikipedia.org/wiki/List_of_WLAN_channels */
 
-        channel = (freq - 5000) / 5;
-
-        if (channel >= 36 && channel <= 48)
-            center_channel = 42;
-        else if (channel >= 52 && channel <= 64)
-            center_channel = 58;
-        else if (channel >= 100 && channel <= 112)
-            center_channel = 106;
-        else if (channel >= 116 && channel <= 128)
-            center_channel = 122;
-        else if (channel >= 132 && channel <= 144)
-            center_channel = 138;
-        else if (channel >= 149 && channel <= 161)
-            center_channel = 155;
-        else if (channel >= 165 && channel <= 177)
-            center_channel = 171;
-
-        if (center_channel) {
+        if (freq > 5950) {
+            /* 6 GHz */
+            channel = (freq - 5950) / 5;
+            channel = ((channel - 1) / 16) * 16 + 7;
+
             *out_ht40             = 1;
             *out_max_oper_chwidth = 1;
-            *out_center_freq      = 5000 + 5 * center_channel;
+            *out_center_freq      = 5950 + 5 * channel;
+        } else {
+            /* 5 GHz */
+            if (freq < 5000) {
+                /* the setting is not valid */
+                nm_assert_not_reached();
+                return;
+            }
+            channel = (freq - 5000) / 5;
+
+            if (channel >= 36 && channel <= 48)
+                center_channel = 42;
+            else if (channel >= 52 && channel <= 64)
+                center_channel = 58;
+            else if (channel >= 100 && channel <= 112)
+                center_channel = 106;
+            else if (channel >= 116 && channel <= 128)
+                center_channel = 122;
+            else if (channel >= 132 && channel <= 144)
+                center_channel = 138;
+            else if (channel >= 149 && channel <= 161)
+                center_channel = 155;
+            else if (channel >= 165 && channel <= 177)
+                center_channel = 171;
+
+            if (center_channel) {
+                *out_ht40             = 1;
+                *out_max_oper_chwidth = 1;
+                *out_center_freq      = 5000 + 5 * center_channel;
+            }
         }
-
         return;
     }
 
@@ -711,10 +736,7 @@ nm_supplicant_config_add_setting_wireless(NMSupplicantConfig *self,
         } else {
             const char *freqs = NULL;
 
-            if (nm_streq(band, "a"))
-                freqs = wifi_freqs_to_string(FALSE);
-            else if (nm_streq(band, "bg"))
-                freqs = wifi_freqs_to_string(TRUE);
+            freqs = wifi_freqs_to_string(band);
 
             if (freqs
                 && !nm_supplicant_config_add_option(self,
@@ -1575,8 +1597,14 @@ nm_supplicant_config_add_setting_8021x(NMSupplicantConfig *self,
     g_string_free(phase2, TRUE);
 
     /* PAC file */
-    path = nm_setting_802_1x_get_pac_file(setting);
-    if (path) {
+    path  = nm_setting_802_1x_get_pac_file(setting);
+    bytes = priv->private_user && path ? nm_g_hash_table_lookup(files, path) : NULL;
+    if (bytes) {
+        if (!nm_supplicant_config_add_blob_for_connection(self, bytes, "pac_file", con_uuid, error))
+            return FALSE;
+    } else if (path) {
+        /* Private connections cannot use paths */
+        g_return_val_if_fail(!priv->private_user, FALSE);
         if (!add_string_val(self, path, "pac_file", FALSE, NULL, error))
             return FALSE;
     } else {
diff --git a/src/core/supplicant/nm-supplicant-interface.c b/src/core/supplicant/nm-supplicant-interface.c
index 5c60a7b6..4476c701 100644
--- a/src/core/supplicant/nm-supplicant-interface.c
+++ b/src/core/supplicant/nm-supplicant-interface.c
@@ -67,6 +67,7 @@ enum {
     GROUP_STARTED,   /* a new Group (interface) was created */
     GROUP_FINISHED,  /* a Group (interface) has been finished */
     PSK_MISMATCH,    /* supplicant reported incorrect PSK */
+    SAE_MISMATCH,    /* supplicant reported incorrect SAE Password */
     LAST_SIGNAL
 };
 
@@ -3237,6 +3238,11 @@ _signal_handle(NMSupplicantInterface *self,
             g_signal_emit(self, signals[PSK_MISMATCH], 0);
             return;
         }
+
+        if (nm_streq(signal_name, "SaePasswordMismatch")) {
+            g_signal_emit(self, signals[SAE_MISMATCH], 0);
+            return;
+        }
         return;
     }
 
@@ -3879,4 +3885,13 @@ nm_supplicant_interface_class_init(NMSupplicantInterfaceClass *klass)
                                          NULL,
                                          G_TYPE_NONE,
                                          0);
+    signals[SAE_MISMATCH] = g_signal_new(NM_SUPPLICANT_INTERFACE_SAE_MISMATCH,
+                                         G_OBJECT_CLASS_TYPE(object_class),
+                                         G_SIGNAL_RUN_LAST,
+                                         0,
+                                         NULL,
+                                         NULL,
+                                         NULL,
+                                         G_TYPE_NONE,
+                                         0);
 }
diff --git a/src/core/supplicant/nm-supplicant-interface.h b/src/core/supplicant/nm-supplicant-interface.h
index 961de1b1..e77f5a42 100644
--- a/src/core/supplicant/nm-supplicant-interface.h
+++ b/src/core/supplicant/nm-supplicant-interface.h
@@ -87,6 +87,7 @@ typedef enum {
 #define NM_SUPPLICANT_INTERFACE_GROUP_STARTED   "group-started"
 #define NM_SUPPLICANT_INTERFACE_GROUP_FINISHED  "group-finished"
 #define NM_SUPPLICANT_INTERFACE_PSK_MISMATCH    "wpa-psk-mismatch"
+#define NM_SUPPLICANT_INTERFACE_SAE_MISMATCH    "wpa-sae-password-mismatch"
 
 typedef struct _NMSupplicantInterfaceClass NMSupplicantInterfaceClass;
 
diff --git a/src/core/supplicant/nm-supplicant-settings-verify.c b/src/core/supplicant/nm-supplicant-settings-verify.c
index cca53d82..a1f888ec 100644
--- a/src/core/supplicant/nm-supplicant-settings-verify.c
+++ b/src/core/supplicant/nm-supplicant-settings-verify.c
@@ -6,6 +6,7 @@
 #include "src/core/nm-default-daemon.h"
 
 #include "nm-supplicant-settings-verify.h"
+#include "libnm-core-aux-intern/nm-libnm-core-utils.h"
 
 #include <stdio.h>
 #include <stdlib.h>
@@ -71,7 +72,7 @@ static const struct Opt opt_table[] = {
     OPT_BYTES("engine_id", 0),
     OPT_INT("fragment_size", 1, 2000),
     OPT_KEYWORD("freq_list", NULL),
-    OPT_INT("frequency", 2412, 5825),
+    OPT_INT("frequency", _NM_WIFI_FREQ_MIN, _NM_WIFI_FREQ_MAX),
     OPT_KEYWORD("group", NM_MAKE_STRV("CCMP", "TKIP", "WEP104", "WEP40", "GCMP-256", )),
     OPT_INT("ht40", 0, 1),
     OPT_BYTES("identity", 0),
diff --git a/src/core/tests/config/NetworkManager-warn.conf b/src/core/tests/config/NetworkManager-warn.conf
index 80df7c52..f43f4cf1 100644
--- a/src/core/tests/config/NetworkManager-warn.conf
+++ b/src/core/tests/config/NetworkManager-warn.conf
@@ -1,5 +1,5 @@
 [main]
-dhcp=dhclient
+dhcp=internal
 plugin=foo,bar,baz
 no-auto-default=11:11:11:11:11:11
 rc-managed=unmanaged
diff --git a/src/core/tests/config/NetworkManager.conf b/src/core/tests/config/NetworkManager.conf
index ae9f3e46..7584a0f2 100644
--- a/src/core/tests/config/NetworkManager.conf
+++ b/src/core/tests/config/NetworkManager.conf
@@ -1,5 +1,5 @@
 [main]
-dhcp=dhclient
+dhcp=internal
 plugins=foo,bar,baz
 no-auto-default=11:11:11:11:11:11
 
diff --git a/src/core/tests/config/test-config.c b/src/core/tests/config/test-config.c
index b2f29821..c549bcaa 100644
--- a/src/core/tests/config/test-config.c
+++ b/src/core/tests/config/test-config.c
@@ -162,7 +162,7 @@ test_config_simple(void)
     g_assert_cmpstr(nm_config_data_get_config_main_file(nm_config_get_data_orig(config)),
                     ==,
                     TEST_DIR "/NetworkManager.conf");
-    g_assert_cmpstr(_config_get_dhcp_client_a(config), ==, "dhclient");
+    g_assert_cmpstr(_config_get_dhcp_client_a(config), ==, "internal");
     g_assert_cmpstr(nm_config_get_log_level(config), ==, "INFO");
     g_assert_cmpint(nm_config_data_get_connectivity_interval(nm_config_get_data_orig(config)),
                     ==,
@@ -297,7 +297,7 @@ test_config_override(void)
     g_assert_cmpstr(nm_config_data_get_config_main_file(nm_config_get_data_orig(config)),
                     ==,
                     TEST_DIR "/NetworkManager.conf");
-    g_assert_cmpstr(_config_get_dhcp_client_a(config), ==, "dhclient");
+    g_assert_cmpstr(_config_get_dhcp_client_a(config), ==, "internal");
     g_assert_cmpstr(nm_config_get_log_level(config), ==, "INFO");
     g_assert_cmpint(nm_config_data_get_connectivity_interval(nm_config_get_data_orig(config)),
                     ==,
diff --git a/src/core/tests/test-core.c b/src/core/tests/test-core.c
index e08296c2..b8920558 100644
--- a/src/core/tests/test-core.c
+++ b/src/core/tests/test-core.c
@@ -7,6 +7,7 @@
 
 #include <net/if.h>
 #include <byteswap.h>
+#include <netinet/ip6.h>
 
 /* need math.h for isinf() and INFINITY. No need to link with -lm */
 #include <math.h>
@@ -19,6 +20,7 @@
 #include "dns/nm-dns-manager.h"
 #include "nm-connectivity.h"
 #include "nm-firewall-utils.h"
+#include "nm-l3-config-data.h"
 
 #include "nm-test-utils-core.h"
 
@@ -2770,6 +2772,104 @@ test_nm_firewall_nft_stdio_mlag(void)
        "nm-mlag-bond0\012delete table netdev nm-mlag-bond0\012");
 }
 
+static void
+test_icmp6_checksum(void)
+{
+    struct ip6_hdr ip6h = {};
+    guint8        *data;
+    guint16        c;
+
+    ip6h.ip6_src = NM_IN6ADDR_INIT(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0);
+    ip6h.ip6_dst = NM_IN6ADDR_INIT(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0);
+    data         = (guint8[]) {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
+    c            = nm_utils_icmp6_checksum(&ip6h.ip6_src, 12, data);
+    g_assert_cmpint(c, ==, htons(0xffb9));
+
+    ip6h.ip6_src = NM_IN6ADDR_INIT(0xfe,
+                                   0x80,
+                                   0x00,
+                                   0x00,
+                                   0x00,
+                                   0x00,
+                                   0x00,
+                                   0x00,
+                                   0xc0,
+                                   0x60,
+                                   0x8c,
+                                   0xaf,
+                                   0xf6,
+                                   0x9b,
+                                   0xe4,
+                                   0x1a);
+    ip6h.ip6_dst = NM_IN6ADDR_INIT(0x20,
+                                   0x02,
+                                   0xaa,
+                                   0xaa,
+                                   0x00,
+                                   0x00,
+                                   0x00,
+                                   0x00,
+                                   0x64,
+                                   0xd4,
+                                   0x29,
+                                   0x32,
+                                   0x35,
+                                   0x85,
+                                   0x7c,
+                                   0x89);
+    data         = (guint8[]) {0xdc, 0x74, 0x1a, 0xcc, 0xd3, 0x8e, 0xca, 0x34};
+    c            = nm_utils_icmp6_checksum(&ip6h.ip6_src, 8, data);
+    g_assert_cmpint(c, ==, htons(0x39af));
+}
+
+/*****************************************************************************/
+
+static void
+test_l3_config_data_cmp_default_routes(void)
+{
+    nm_auto_unref_dedup_multi_index NMDedupMultiIndex *multi_idx = nm_dedup_multi_index_new();
+    nm_auto_unref_l3cd_init NML3ConfigData            *a         = NULL;
+    nm_auto_unref_l3cd_init NML3ConfigData            *b         = NULL;
+    const int                                          IFINDEX   = 1;
+
+    a = nm_l3_config_data_new(multi_idx, IFINDEX, NM_IP_CONFIG_SOURCE_USER);
+    nm_l3_config_data_add_route_4(
+        a,
+        NM_PLATFORM_IP4_ROUTE_INIT(.ifindex = IFINDEX,
+                                   .network = 0,
+                                   .plen    = 0,
+                                   .gateway = nmtst_inet4_from_string("192.168.1.1"),
+                                   .metric  = 100));
+
+    b = nm_l3_config_data_new(multi_idx, IFINDEX, NM_IP_CONFIG_SOURCE_USER);
+    nm_l3_config_data_add_route_4(
+        b,
+        NM_PLATFORM_IP4_ROUTE_INIT(.ifindex       = IFINDEX,
+                                   .network       = 0,
+                                   .plen          = 0,
+                                   .gateway       = nmtst_inet4_from_string("192.168.1.1"),
+                                   .metric        = 100,
+                                   .table_coerced = nm_platform_route_table_coerce(100)));
+
+    nm_l3_config_data_seal(a);
+    nm_l3_config_data_seal(b);
+
+    g_assert(nm_l3_config_data_get_best_default_route(a, AF_INET));
+    g_assert(!nm_l3_config_data_get_best_default_route(b, AF_INET));
+
+    g_assert_cmpint(nm_l3_config_data_cmp_full(a, b, NM_L3_CONFIG_CMP_FLAGS_ROUTES_ID), !=, 0);
+    g_assert_cmpint(nm_l3_config_data_cmp_full(b, a, NM_L3_CONFIG_CMP_FLAGS_ROUTES_ID), !=, 0);
+
+    g_assert_cmpint(nm_l3_config_data_cmp_full(a, b, NM_L3_CONFIG_CMP_FLAGS_ROUTES), !=, 0);
+    g_assert_cmpint(nm_l3_config_data_cmp_full(b, a, NM_L3_CONFIG_CMP_FLAGS_ROUTES), !=, 0);
+
+    g_assert_cmpint(nm_l3_config_data_cmp_full(a, b, NM_L3_CONFIG_CMP_FLAGS_ADDRESSES), ==, 0);
+    g_assert_cmpint(nm_l3_config_data_cmp_full(b, a, NM_L3_CONFIG_CMP_FLAGS_ADDRESSES), ==, 0);
+
+    g_assert_cmpint(nm_l3_config_data_cmp_full(a, a, NM_L3_CONFIG_CMP_FLAGS_ALL), ==, 0);
+    g_assert_cmpint(nm_l3_config_data_cmp_full(b, b, NM_L3_CONFIG_CMP_FLAGS_ALL), ==, 0);
+}
+
 /*****************************************************************************/
 
 NMTST_DEFINE();
@@ -2848,5 +2948,10 @@ main(int argc, char **argv)
 
     g_test_add_func("/core/test_nm_firewall_nft_stdio_mlag", test_nm_firewall_nft_stdio_mlag);
 
+    g_test_add_func("/core/general/test_icmp6_checksum", test_icmp6_checksum);
+
+    g_test_add_func("/core/general/test_l3_config_data_cmp_default_routes",
+                    test_l3_config_data_cmp_default_routes);
+
     return g_test_run();
 }
diff --git a/src/core/tests/test-netns.c b/src/core/tests/test-netns.c
index 26ecbcb8..7bcd3809 100644
--- a/src/core/tests/test-netns.c
+++ b/src/core/tests/test-netns.c
@@ -53,6 +53,44 @@ test_ip_reservation_shared4(void)
     }
 }
 
+static void
+test_ip_reservation_clat(void)
+{
+    gs_unref_object NMPlatform *platform = NULL;
+    gs_unref_object NMNetns    *netns    = NULL;
+    NMNetnsIPReservation       *res[8];
+    NMNetnsIPReservation       *res1;
+    char                        buf[NM_INET_ADDRSTRLEN];
+    guint                       i;
+
+    platform = g_object_ref(NM_PLATFORM_GET);
+    netns    = nm_netns_new(platform);
+
+    /* Allocate addresses 192.0.0.{5,6,7,0,1,2,3,4} */
+    for (i = 0; i < 8; i++) {
+        res[i] = nm_netns_ip_reservation_get(netns, NM_NETNS_IP_RESERVATION_TYPE_CLAT);
+        g_snprintf(buf, sizeof(buf), "192.0.0.%u", (i + 5) % 8);
+        nmtst_assert_ip4_address(res[i]->addr, buf);
+        g_assert_cmpint(res[i]->_ref_count, ==, 1);
+    }
+
+    /* Release an address and get it back */
+    nm_netns_ip_reservation_release(res[2]);
+    res[2] = nm_netns_ip_reservation_get(netns, NM_NETNS_IP_RESERVATION_TYPE_CLAT);
+    nmtst_assert_ip4_address(res[2]->addr, "192.0.0.7");
+
+    /* No reuse */
+    NMTST_EXPECT_NM_ERROR("netns[*]: clat: ran out of IP addresses");
+    res1 = nm_netns_ip_reservation_get(netns, NM_NETNS_IP_RESERVATION_TYPE_CLAT);
+    g_test_assert_expected_messages();
+    g_assert_null(res1);
+
+    /* Release all */
+    for (i = 0; i < 8; i++) {
+        nm_netns_ip_reservation_release(res[i]);
+    }
+}
+
 /*****************************************************************************/
 
 NMTST_DEFINE();
@@ -64,6 +102,7 @@ main(int argc, char **argv)
     nm_linux_platform_setup();
 
     g_test_add_func("/netns/ip_reservation/shared4", test_ip_reservation_shared4);
+    g_test_add_func("/netns/ip_reservation/clat", test_ip_reservation_clat);
 
     return g_test_run();
 }
diff --git a/src/core/tests/test-systemd.c b/src/core/tests/test-systemd.c
index 1b0b7f65..09481a64 100644
--- a/src/core/tests/test-systemd.c
+++ b/src/core/tests/test-systemd.c
@@ -83,6 +83,42 @@ test_sd_event(void)
 
 /*****************************************************************************/
 
+static void
+test_http_url_is_valid_https(void)
+{
+    /* CVE-2026-10805: connection.mud-url is pasted verbatim into the dhclient
+     * config inside a quoted string ("send mudurl \"%s\";"). This function
+     * gates the property at verify() time, so it must reject characters that
+     * break out of the quotes or inject config syntax. */
+#define _assert_valid(url)   g_assert(nm_sd_http_url_is_valid_https("" url))
+#define _assert_invalid(url) g_assert(!nm_sd_http_url_is_valid_https("" url))
+
+    _assert_valid("https://example.com/mud.json");
+    _assert_valid("https://example.com");
+    _assert_valid("https://example.com/a?b=c&d=e#frag");
+    _assert_valid("https://[2001:db8::1]/x");
+    _assert_valid("https://user@example.com/~p/(a)*,;=+!$'");
+    _assert_valid("https://user:pass@example.com/p%20q?x=%2F");
+
+    _assert_invalid("http://example.com");
+    _assert_invalid("ftp://example.com");
+    _assert_invalid("example.com");
+    _assert_invalid("");
+    _assert_invalid("https://");
+
+    _assert_invalid("https://example.com/\""); /* breaks out of the quoted string */
+    _assert_invalid("https://example.com/\\"); /* escapes the following char */
+    _assert_invalid("https://example.com/\n");
+    _assert_invalid("https://example.com/\t");
+    _assert_invalid("https://example.com/a\x01b");
+    _assert_invalid("https://example.com/\xc3\xa4"); /* non-ASCII */
+
+#undef _assert_valid
+#undef _assert_invalid
+}
+
+/*****************************************************************************/
+
 NMTST_DEFINE();
 
 int
@@ -91,6 +127,7 @@ main(int argc, char **argv)
     nmtst_init(&argc, &argv, TRUE);
 
     g_test_add_func("/systemd/sd-event", test_sd_event);
+    g_test_add_func("/systemd/http-url-is-valid-https", test_http_url_is_valid_https);
 
     return g_test_run();
 }
diff --git a/src/core/vpn/nm-vpn-connection.c b/src/core/vpn/nm-vpn-connection.c
index 54478c53..4623af9c 100644
--- a/src/core/vpn/nm-vpn-connection.c
+++ b/src/core/vpn/nm-vpn-connection.c
@@ -2135,16 +2135,20 @@ _dbus_signal_ip_config_cb(NMVpnConnection *self, int addr_family, GVariant *dict
                          IS_IPv4 ? NM_VPN_PLUGIN_IP4_CONFIG_DOMAIN
                                  : NM_VPN_PLUGIN_IP6_CONFIG_DOMAIN,
                          "&s",
-                         &v_str))
+                         &v_str)) {
         nm_l3_config_data_add_domain(l3cd, addr_family, v_str);
+        nm_l3_config_data_add_search(l3cd, addr_family, v_str);
+    }
 
     if (g_variant_lookup(dict,
                          IS_IPv4 ? NM_VPN_PLUGIN_IP4_CONFIG_DOMAINS
                                  : NM_VPN_PLUGIN_IP6_CONFIG_DOMAINS,
                          "as",
                          &var_iter)) {
-        while (g_variant_iter_next(var_iter, "&s", &v_str))
+        while (g_variant_iter_next(var_iter, "&s", &v_str)) {
             nm_l3_config_data_add_domain(l3cd, addr_family, v_str);
+            nm_l3_config_data_add_search(l3cd, addr_family, v_str);
+        }
         g_variant_iter_free(var_iter);
     }
 
diff --git a/src/core/vpn/nm-vpn-manager.c b/src/core/vpn/nm-vpn-manager.c
index 6bf8edae..e49c6898 100644
--- a/src/core/vpn/nm-vpn-manager.c
+++ b/src/core/vpn/nm-vpn-manager.c
@@ -60,16 +60,21 @@ nm_vpn_manager_activate_connection(NMVpnManager *manager, NMVpnConnection *vpn,
 {
     NMVpnManagerPrivate *priv;
     NMVpnPluginInfo     *plugin_info;
+    NMConnection        *applied;
     const char          *service_name;
     NMDevice            *device;
+    const char          *user;
 
     g_return_val_if_fail(NM_IS_VPN_MANAGER(manager), FALSE);
     g_return_val_if_fail(NM_IS_VPN_CONNECTION(vpn), FALSE);
     g_return_val_if_fail(!error || !*error, FALSE);
 
-    priv   = NM_VPN_MANAGER_GET_PRIVATE(manager);
-    device = nm_active_connection_get_device(NM_ACTIVE_CONNECTION(vpn));
-    g_assert(device);
+    priv    = NM_VPN_MANAGER_GET_PRIVATE(manager);
+    device  = nm_active_connection_get_device(NM_ACTIVE_CONNECTION(vpn));
+    applied = nm_active_connection_get_applied_connection(NM_ACTIVE_CONNECTION(vpn));
+    nm_assert(device);
+    nm_assert(applied);
+
     if (nm_device_get_state(device) != NM_DEVICE_STATE_ACTIVATED
         && nm_device_get_state(device) != NM_DEVICE_STATE_SECONDARIES) {
         g_set_error_literal(error,
@@ -101,6 +106,30 @@ nm_vpn_manager_activate_connection(NMVpnManager *manager, NMVpnConnection *vpn,
         return FALSE;
     }
 
+    user = nm_utils_get_connection_first_permissions_user(applied);
+    if (user) {
+        NMSettingConnection *s_con;
+
+        s_con = nm_connection_get_setting_connection(applied);
+        nm_assert(s_con);
+        if (_nm_setting_connection_get_num_permissions_users(s_con) > 1) {
+            g_set_error_literal(error,
+                                NM_MANAGER_ERROR,
+                                NM_MANAGER_ERROR_CONNECTION_NOT_AVAILABLE,
+                                "private VPN connections with multiple users are not allowed.");
+            return FALSE;
+        }
+
+        if (!nm_vpn_plugin_info_supports_safe_private_file_access(plugin_info)) {
+            g_set_error(error,
+                        NM_MANAGER_ERROR,
+                        NM_MANAGER_ERROR_CONNECTION_NOT_AVAILABLE,
+                        "The '%s' plugin doesn't support private connections.",
+                        nm_vpn_plugin_info_get_name(plugin_info));
+            return FALSE;
+        }
+    }
+
     nm_vpn_connection_activate(vpn, plugin_info);
 
     if (!nm_vpn_plugin_info_supports_multiple(plugin_info)) {