summary refs log tree commit diff
diff options
context:
space:
mode:
authorMichael Biebl <biebl@debian.org>2025-09-12 19:47:07 +0200
committerMichael Biebl <biebl@debian.org>2025-09-12 19:47:07 +0200
commitbc5f10851bffd6af1c2855635be7a28b6dba3195 (patch)
treed47255895378efcdc0a32c2ddeed055f76935654
parent7e9091a5960f67a84787c03153324915220e4816 (diff)
New upstream version 1.54.1 upstream/1.54.1
-rw-r--r--.gitlab-ci.yml312
-rw-r--r--.gitlab-ci/config.yml32
-rwxr-xr-x.gitlab-ci/fedora-install.sh7
-rwxr-xr-x.gitlab-ci/run-test.sh13
-rw-r--r--NEWS11
-rwxr-xr-xcontrib/alpine/REQUIRED_PACKAGES1
-rw-r--r--contrib/fedora/rpm/NetworkManager.spec2
-rwxr-xr-xcontrib/scripts/nm-ci-run.sh8
-rw-r--r--man/NetworkManager.conf.xml12
-rw-r--r--man/nm-initrd-generator.xml21
-rw-r--r--meson.build2
-rw-r--r--src/core/devices/nm-device-bond.c3
-rw-r--r--src/core/devices/nm-device.c25
-rw-r--r--src/core/devices/wifi/nm-device-wifi.c51
-rw-r--r--src/core/dns/nm-dns-manager.c78
-rw-r--r--src/core/nm-config-data.c38
-rw-r--r--src/core/nm-config-data.h1
-rw-r--r--src/core/nm-config.c14
-rw-r--r--src/core/nm-l3-config-data.c23
-rw-r--r--src/core/nm-l3-config-data.h3
-rw-r--r--src/core/nm-manager.c13
-rw-r--r--src/core/tests/config/global-dns-empty.conf3
-rw-r--r--src/core/tests/config/global-dns-not-set.conf5
-rw-r--r--src/core/tests/config/test-config.c22
-rw-r--r--src/libnm-client-impl/tests/test-nm-client.c14
-rw-r--r--src/libnm-core-impl/nm-setting-wireguard.c19
-rw-r--r--src/libnm-core-impl/tests/test-setting.c219
-rw-r--r--src/nm-initrd-generator/nmi-cmdline-reader.c65
-rw-r--r--src/nm-initrd-generator/nmi-nbft-reader.c32
-rw-r--r--src/nm-initrd-generator/tests/meson.build4
-rw-r--r--src/nm-initrd-generator/tests/nbft-ipv4-dhcp/firmware/acpi/tables/NBFT-Dell.PowerEdge.R760bin0 -> 1017 bytes
-rw-r--r--src/nm-initrd-generator/tests/nbft-ipv4-dhcp/firmware/acpi/tables/NBFT-dhcp-ipv4bin0 -> 825 bytes
-rw-r--r--src/nm-initrd-generator/tests/nbft-ipv4-static/firmware/acpi/tables/NBFT-Dell.PowerEdge.R660-fw1.5.5-mpath+discoverybin0 -> 4147 bytes
-rw-r--r--src/nm-initrd-generator/tests/nbft-ipv4-static/firmware/acpi/tables/NBFT-multibin0 -> 2589 bytes
-rw-r--r--src/nm-initrd-generator/tests/nbft-ipv4-static/firmware/acpi/tables/NBFT-rhpocbin0 -> 724 bytes
-rw-r--r--src/nm-initrd-generator/tests/nbft-ipv4-static/firmware/acpi/tables/NBFT-static-ipv4bin0 -> 725 bytes
-rw-r--r--src/nm-initrd-generator/tests/nbft-ipv6-auto/firmware/acpi/tables/NBFT-auto-ipv6bin0 -> 721 bytes
-rw-r--r--src/nm-initrd-generator/tests/nbft-ipv6-auto/firmware/acpi/tables/NBFT-dhcp-ipv6bin0 -> 725 bytes
-rw-r--r--src/nm-initrd-generator/tests/nbft-ipv6-auto/firmware/acpi/tables/NBFT-ipv6-noip+discbin0 -> 785 bytes
-rw-r--r--src/nm-initrd-generator/tests/nbft-ipv6-static/firmware/acpi/tables/NBFT-static-ipv6bin0 -> 721 bytes
-rw-r--r--src/nm-initrd-generator/tests/nbft-vlan/firmware/acpi/tables/NBFT-qemu-vlans-incompletebin0 -> 2200 bytes
-rw-r--r--src/nm-initrd-generator/tests/test-cmdline-reader.c144
-rw-r--r--src/nm-initrd-generator/tests/test-nbft-reader.c397
-rwxr-xr-xsrc/tests/client/test-client.py6
44 files changed, 1290 insertions, 310 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index a90f3e8b..a938fa10 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -60,11 +60,11 @@ variables:
   #
   # This is done by running `ci-fairy generate-template` and possibly bumping
   # ".default_tag".
-  ALPINE_TAG:  'tag-da1ae96102c6'
-  CENTOS_TAG:  'tag-c8df7d2b249f'
-  DEBIAN_TAG:  'tag-e68f538711ec'
-  FEDORA_TAG:  'tag-c8df7d2b249f'
-  UBUNTU_TAG:  'tag-e68f538711ec'
+  ALPINE_TAG:  'tag-dcc430216167'
+  CENTOS_TAG:  'tag-feb1adbc208e'
+  DEBIAN_TAG:  'tag-afb784497c2f'
+  FEDORA_TAG:  'tag-feb1adbc208e'
+  UBUNTU_TAG:  'tag-afb784497c2f'
 
   ALPINE_EXEC: 'bash .gitlab-ci/alpine-install.sh'
   CENTOS_EXEC: 'bash .gitlab-ci/fedora-install.sh'
@@ -102,25 +102,25 @@ variables:
 # Build a container for each distribution + version. The ci-templates
 # will re-use the containers if the tag doesn't change.
 
-tier1:fedora:42@prep:
+tier1:fedora:43@prep:
   extends:
     - .fdo.container-build@fedora
   stage: prep
   variables:
     GIT_STRATEGY: none
-    FDO_DISTRIBUTION_VERSION: '42'
+    FDO_DISTRIBUTION_VERSION: '43'
     FDO_DISTRIBUTION_TAG: $FEDORA_TAG
     FDO_DISTRIBUTION_EXEC: $FEDORA_EXEC
   rules:
     - if: $CI_PIPELINE_SOURCE != 'schedule' || $SCHEDULED_PIPELINE_NAME == "weekly"
 
-tier2:fedora:rawhide@prep:
+tier3:fedora:42@prep:
   extends:
     - .fdo.container-build@fedora
   stage: prep
   variables:
     GIT_STRATEGY: none
-    FDO_DISTRIBUTION_VERSION: 'rawhide'
+    FDO_DISTRIBUTION_VERSION: '42'
     FDO_DISTRIBUTION_TAG: $FEDORA_TAG
     FDO_DISTRIBUTION_EXEC: $FEDORA_EXEC
   rules:
@@ -128,76 +128,6 @@ tier2:fedora:rawhide@prep:
       when: manual
       allow_failure: true
 
-tier2:centos:stream9@prep:
-  extends:
-    - .fdo.container-build@centos
-  stage: prep
-  variables:
-    GIT_STRATEGY: none
-    FDO_DISTRIBUTION_VERSION: 'stream9'
-    FDO_DISTRIBUTION_TAG: $CENTOS_TAG
-    FDO_DISTRIBUTION_EXEC: $CENTOS_EXEC
-  rules:
-    - if: $CI_PIPELINE_SOURCE != 'schedule'
-      when: manual
-      allow_failure: true
-
-tier2:ubuntu:devel@prep:
-  extends:
-    - .fdo.container-build@ubuntu
-  stage: prep
-  variables:
-    GIT_STRATEGY: none
-    FDO_DISTRIBUTION_VERSION: 'devel'
-    FDO_DISTRIBUTION_TAG: $UBUNTU_TAG
-    FDO_DISTRIBUTION_EXEC: $UBUNTU_EXEC
-  rules:
-    - if: $CI_PIPELINE_SOURCE != 'schedule'
-      when: manual
-      allow_failure: true
-
-tier2:debian:testing@prep:
-  extends:
-    - .fdo.container-build@debian
-  stage: prep
-  variables:
-    GIT_STRATEGY: none
-    FDO_DISTRIBUTION_VERSION: 'testing'
-    FDO_DISTRIBUTION_TAG: $DEBIAN_TAG
-    FDO_DISTRIBUTION_EXEC: $DEBIAN_EXEC
-  rules:
-    - if: $CI_PIPELINE_SOURCE != 'schedule'
-      when: manual
-      allow_failure: true
-
-tier2:debian:sid@prep:
-  extends:
-    - .fdo.container-build@debian
-  stage: prep
-  variables:
-    GIT_STRATEGY: none
-    FDO_DISTRIBUTION_VERSION: 'sid'
-    FDO_DISTRIBUTION_TAG: $DEBIAN_TAG
-    FDO_DISTRIBUTION_EXEC: $DEBIAN_EXEC
-  rules:
-    - if: $CI_PIPELINE_SOURCE != 'schedule'
-      when: manual
-      allow_failure: true
-
-tier2:alpine:edge@prep:
-  extends:
-    - .fdo.container-build@alpine
-  stage: prep
-  variables:
-    GIT_STRATEGY: none
-    FDO_DISTRIBUTION_VERSION: 'edge'
-    FDO_DISTRIBUTION_TAG: $ALPINE_TAG
-    FDO_DISTRIBUTION_EXEC: $ALPINE_EXEC
-  rules:
-    - if: $CI_PIPELINE_SOURCE != 'schedule'
-      when: manual
-      allow_failure: true
-
 tier3:fedora:41@prep:
   extends:
     - .fdo.container-build@fedora
@@ -226,13 +156,13 @@ tier3:ubuntu:25.04@prep:
       when: manual
       allow_failure: true
 
-tier3:ubuntu:24.10@prep:
+tier3:ubuntu:24.04@prep:
   extends:
     - .fdo.container-build@ubuntu
   stage: prep
   variables:
     GIT_STRATEGY: none
-    FDO_DISTRIBUTION_VERSION: '24.10'
+    FDO_DISTRIBUTION_VERSION: '24.04'
     FDO_DISTRIBUTION_TAG: $UBUNTU_TAG
     FDO_DISTRIBUTION_EXEC: $UBUNTU_EXEC
   rules:
@@ -240,13 +170,13 @@ tier3:ubuntu:24.10@prep:
       when: manual
       allow_failure: true
 
-tier3:ubuntu:24.04@prep:
+tier3:ubuntu:22.04@prep:
   extends:
     - .fdo.container-build@ubuntu
   stage: prep
   variables:
     GIT_STRATEGY: none
-    FDO_DISTRIBUTION_VERSION: '24.04'
+    FDO_DISTRIBUTION_VERSION: '22.04'
     FDO_DISTRIBUTION_TAG: $UBUNTU_TAG
     FDO_DISTRIBUTION_EXEC: $UBUNTU_EXEC
   rules:
@@ -254,15 +184,15 @@ tier3:ubuntu:24.04@prep:
       when: manual
       allow_failure: true
 
-tier3:ubuntu:22.04@prep:
+tier3:debian:13@prep:
   extends:
-    - .fdo.container-build@ubuntu
+    - .fdo.container-build@debian
   stage: prep
   variables:
     GIT_STRATEGY: none
-    FDO_DISTRIBUTION_VERSION: '22.04'
-    FDO_DISTRIBUTION_TAG: $UBUNTU_TAG
-    FDO_DISTRIBUTION_EXEC: $UBUNTU_EXEC
+    FDO_DISTRIBUTION_VERSION: '13'
+    FDO_DISTRIBUTION_TAG: $DEBIAN_TAG
+    FDO_DISTRIBUTION_EXEC: $DEBIAN_EXEC
   rules:
     - if: $CI_PIPELINE_SOURCE != 'schedule'
       when: manual
@@ -282,6 +212,20 @@ tier3:debian:12@prep:
       when: manual
       allow_failure: true
 
+tier3:alpine:3.22@prep:
+  extends:
+    - .fdo.container-build@alpine
+  stage: prep
+  variables:
+    GIT_STRATEGY: none
+    FDO_DISTRIBUTION_VERSION: '3.22'
+    FDO_DISTRIBUTION_TAG: $ALPINE_TAG
+    FDO_DISTRIBUTION_EXEC: $ALPINE_EXEC
+  rules:
+    - if: $CI_PIPELINE_SOURCE != 'schedule'
+      when: manual
+      allow_failure: true
+
 tier3:alpine:3.21@prep:
   extends:
     - .fdo.container-build@alpine
@@ -324,6 +268,34 @@ tier3:alpine:3.19@prep:
       when: manual
       allow_failure: true
 
+tier3:centos:stream10@prep:
+  extends:
+    - .fdo.container-build@centos
+  stage: prep
+  variables:
+    GIT_STRATEGY: none
+    FDO_DISTRIBUTION_VERSION: 'stream10'
+    FDO_DISTRIBUTION_TAG: $CENTOS_TAG
+    FDO_DISTRIBUTION_EXEC: $CENTOS_EXEC
+  rules:
+    - if: $CI_PIPELINE_SOURCE != 'schedule'
+      when: manual
+      allow_failure: true
+
+tier3:centos:stream9@prep:
+  extends:
+    - .fdo.container-build@centos
+  stage: prep
+  variables:
+    GIT_STRATEGY: none
+    FDO_DISTRIBUTION_VERSION: 'stream9'
+    FDO_DISTRIBUTION_TAG: $CENTOS_TAG
+    FDO_DISTRIBUTION_EXEC: $CENTOS_EXEC
+  rules:
+    - if: $CI_PIPELINE_SOURCE != 'schedule'
+      when: manual
+      allow_failure: true
+
 #################################################################
 #                                                               #
 # tierN stage                                                   #
@@ -340,7 +312,7 @@ tier3:alpine:3.19@prep:
   dependencies: []
 
 
-t_fedora:42:
+t_fedora:43:
   extends:
     - .build@template
     - .fdo.distribution-image@fedora
@@ -356,94 +328,24 @@ t_fedora:42:
         - tarball
         - subtree
   variables:
-    FDO_DISTRIBUTION_VERSION: '42'
+    FDO_DISTRIBUTION_VERSION: '43'
     FDO_DISTRIBUTION_TAG: $FEDORA_TAG
   needs:
-    - "tier1:fedora:42@prep"
+    - "tier1:fedora:43@prep"
   rules:
     - if: $CI_PIPELINE_SOURCE != 'schedule'
 
-t_fedora:rawhide:
+t_fedora:42:
   extends:
     - .build@template
     - .fdo.distribution-image@fedora
     - .nm_artifacts_debug
-  stage: tier2
+  stage: tier3
   variables:
-    FDO_DISTRIBUTION_VERSION: 'rawhide'
+    FDO_DISTRIBUTION_VERSION: '42'
     FDO_DISTRIBUTION_TAG: $FEDORA_TAG
   needs:
-    - "tier2:fedora:rawhide@prep"
-  rules:
-    - if: $CI_PIPELINE_SOURCE != 'schedule'
-
-t_centos:stream9:
-  extends:
-    - .build@template
-    - .fdo.distribution-image@centos
-    - .nm_artifacts_debug
-  stage: tier2
-  variables:
-    FDO_DISTRIBUTION_VERSION: 'stream9'
-    FDO_DISTRIBUTION_TAG: $CENTOS_TAG
-  needs:
-    - "tier2:centos:stream9@prep"
-  rules:
-    - if: $CI_PIPELINE_SOURCE != 'schedule'
-
-t_ubuntu:devel:
-  extends:
-    - .build@template
-    - .fdo.distribution-image@ubuntu
-    - .nm_artifacts_debug
-  stage: tier2
-  variables:
-    FDO_DISTRIBUTION_VERSION: 'devel'
-    FDO_DISTRIBUTION_TAG: $UBUNTU_TAG
-  needs:
-    - "tier2:ubuntu:devel@prep"
-  rules:
-    - if: $CI_PIPELINE_SOURCE != 'schedule'
-
-t_debian:testing:
-  extends:
-    - .build@template
-    - .fdo.distribution-image@debian
-    - .nm_artifacts_debug
-  stage: tier2
-  variables:
-    FDO_DISTRIBUTION_VERSION: 'testing'
-    FDO_DISTRIBUTION_TAG: $DEBIAN_TAG
-  needs:
-    - "tier2:debian:testing@prep"
-  rules:
-    - if: $CI_PIPELINE_SOURCE != 'schedule'
-
-t_debian:sid:
-  extends:
-    - .build@template
-    - .fdo.distribution-image@debian
-    - .nm_artifacts_debug
-  stage: tier2
-  variables:
-    FDO_DISTRIBUTION_VERSION: 'sid'
-    FDO_DISTRIBUTION_TAG: $DEBIAN_TAG
-  needs:
-    - "tier2:debian:sid@prep"
-  rules:
-    - if: $CI_PIPELINE_SOURCE != 'schedule'
-
-t_alpine:edge:
-  extends:
-    - .build@template
-    - .fdo.distribution-image@alpine
-    - .nm_artifacts_debug
-  stage: tier2
-  variables:
-    FDO_DISTRIBUTION_VERSION: 'edge'
-    FDO_DISTRIBUTION_TAG: $ALPINE_TAG
-  needs:
-    - "tier2:alpine:edge@prep"
+    - "tier3:fedora:42@prep"
   rules:
     - if: $CI_PIPELINE_SOURCE != 'schedule'
 
@@ -475,45 +377,45 @@ t_ubuntu:25.04:
   rules:
     - if: $CI_PIPELINE_SOURCE != 'schedule'
 
-t_ubuntu:24.10:
+t_ubuntu:24.04:
   extends:
     - .build@template
     - .fdo.distribution-image@ubuntu
     - .nm_artifacts_debug
   stage: tier3
   variables:
-    FDO_DISTRIBUTION_VERSION: '24.10'
+    FDO_DISTRIBUTION_VERSION: '24.04'
     FDO_DISTRIBUTION_TAG: $UBUNTU_TAG
   needs:
-    - "tier3:ubuntu:24.10@prep"
+    - "tier3:ubuntu:24.04@prep"
   rules:
     - if: $CI_PIPELINE_SOURCE != 'schedule'
 
-t_ubuntu:24.04:
+t_ubuntu:22.04:
   extends:
     - .build@template
     - .fdo.distribution-image@ubuntu
     - .nm_artifacts_debug
   stage: tier3
   variables:
-    FDO_DISTRIBUTION_VERSION: '24.04'
+    FDO_DISTRIBUTION_VERSION: '22.04'
     FDO_DISTRIBUTION_TAG: $UBUNTU_TAG
   needs:
-    - "tier3:ubuntu:24.04@prep"
+    - "tier3:ubuntu:22.04@prep"
   rules:
     - if: $CI_PIPELINE_SOURCE != 'schedule'
 
-t_ubuntu:22.04:
+t_debian:13:
   extends:
     - .build@template
-    - .fdo.distribution-image@ubuntu
+    - .fdo.distribution-image@debian
     - .nm_artifacts_debug
   stage: tier3
   variables:
-    FDO_DISTRIBUTION_VERSION: '22.04'
-    FDO_DISTRIBUTION_TAG: $UBUNTU_TAG
+    FDO_DISTRIBUTION_VERSION: '13'
+    FDO_DISTRIBUTION_TAG: $DEBIAN_TAG
   needs:
-    - "tier3:ubuntu:22.04@prep"
+    - "tier3:debian:13@prep"
   rules:
     - if: $CI_PIPELINE_SOURCE != 'schedule'
 
@@ -531,6 +433,20 @@ t_debian:12:
   rules:
     - if: $CI_PIPELINE_SOURCE != 'schedule'
 
+t_alpine:3.22:
+  extends:
+    - .build@template
+    - .fdo.distribution-image@alpine
+    - .nm_artifacts_debug
+  stage: tier3
+  variables:
+    FDO_DISTRIBUTION_VERSION: '3.22'
+    FDO_DISTRIBUTION_TAG: $ALPINE_TAG
+  needs:
+    - "tier3:alpine:3.22@prep"
+  rules:
+    - if: $CI_PIPELINE_SOURCE != 'schedule'
+
 t_alpine:3.21:
   extends:
     - .build@template
@@ -573,6 +489,34 @@ t_alpine:3.19:
   rules:
     - if: $CI_PIPELINE_SOURCE != 'schedule'
 
+t_centos:stream10:
+  extends:
+    - .build@template
+    - .fdo.distribution-image@centos
+    - .nm_artifacts_debug
+  stage: tier3
+  variables:
+    FDO_DISTRIBUTION_VERSION: 'stream10'
+    FDO_DISTRIBUTION_TAG: $CENTOS_TAG
+  needs:
+    - "tier3:centos:stream10@prep"
+  rules:
+    - if: $CI_PIPELINE_SOURCE != 'schedule'
+
+t_centos:stream9:
+  extends:
+    - .build@template
+    - .fdo.distribution-image@centos
+    - .nm_artifacts_debug
+  stage: tier3
+  variables:
+    FDO_DISTRIBUTION_VERSION: 'stream9'
+    FDO_DISTRIBUTION_TAG: $CENTOS_TAG
+  needs:
+    - "tier3:centos:stream9@prep"
+  rules:
+    - if: $CI_PIPELINE_SOURCE != 'schedule'
+
 #################################################################
 #                                                               #
 # specific jobs                                                 #
@@ -583,10 +527,10 @@ check-patch:
   extends:
     - .fdo.distribution-image@fedora
   variables:
-    FDO_DISTRIBUTION_VERSION: '42'
+    FDO_DISTRIBUTION_VERSION: '43'
     FDO_DISTRIBUTION_TAG: $FEDORA_TAG
   needs:
-    - "tier1:fedora:42@prep"
+    - "tier1:fedora:43@prep"
   rules:
     - if: $CI_PIPELINE_SOURCE != 'schedule'
   stage: tier1
@@ -598,10 +542,10 @@ check-tree:
   extends:
     - .fdo.distribution-image@fedora
   variables:
-    FDO_DISTRIBUTION_VERSION: '42'
+    FDO_DISTRIBUTION_VERSION: '43'
     FDO_DISTRIBUTION_TAG: $FEDORA_TAG
   needs:
-    - "tier1:fedora:42@prep"
+    - "tier1:fedora:43@prep"
   rules:
     - if: $CI_PIPELINE_SOURCE == 'merge_request_event' && $CI_MERGE_REQUEST_TARGET_BRANCH_NAME != $CI_DEFAULT_BRANCH
       allow_failure: true
@@ -631,9 +575,9 @@ pages:
       when: never
     - if: $CI_MERGE_REQUEST_SOURCE_BRANCH_NAME == 'main'
   dependencies:
-    - "t_fedora:42: [meson+gcc+docs+valgrind]"
+    - "t_fedora:43: [meson+gcc+docs+valgrind]"
   needs:
-    - "t_fedora:42: [meson+gcc+docs+valgrind]"
+    - "t_fedora:43: [meson+gcc+docs+valgrind]"
 
 triage:issues:
   stage: triage
@@ -650,11 +594,11 @@ coverity:
   extends:
     - .fdo.distribution-image@fedora
   variables:
-    FDO_DISTRIBUTION_VERSION: '42'
+    FDO_DISTRIBUTION_VERSION: '43'
     FDO_DISTRIBUTION_TAG: $FEDORA_TAG
   stage: coverity
   needs:
-    - "tier1:fedora:42@prep"
+    - "tier1:fedora:43@prep"
   rules:
     - if: $CI_PIPELINE_SOURCE == "schedule" && $SCHEDULED_PIPELINE_NAME == "weekly"
   script:
diff --git a/.gitlab-ci/config.yml b/.gitlab-ci/config.yml
index 526339e2..b99c1673 100644
--- a/.gitlab-ci/config.yml
+++ b/.gitlab-ci/config.yml
@@ -23,52 +23,38 @@ distributions:
   - name: fedora
     tier: 1
     versions:
-      - '42'
+      - '43'
 
   # TIER 2: distribution versions that will or might use the current NM version.
   # Run when doing a release.
-  - name: fedora
-    tier: 2
-    versions:
-      - 'rawhide'
-  - name: centos
-    tier: 2
-    versions:
-      - 'stream9'
-  - name: ubuntu
-    tier: 2
-    versions:
-      - 'devel'
-  - name: debian
-    tier: 2
-    versions:
-      - 'testing'
-      - 'sid'
-  - name: alpine
-    tier: 2
-    versions:
-      - 'edge'
 
   # TIER 3: distribution versions not in EOL but don't use the current NM version.
   # Run when doing a release, but a failure won't be blocking for the release.
   - name: fedora
     tier: 3
     versions:
+      - '42'
       - '41'
   - name: ubuntu
     tier: 3
     versions:
       - '25.04'
-      - '24.10'
       - '24.04'
       - '22.04'
   - name: debian
     tier: 3
     versions:
+      - '13'
       - '12'
   - name: alpine
     tier: 3
     versions:
+      - '3.22'
       - '3.21'
       - '3.20'
       - '3.19'
+  - name: centos
+    tier: 3
+    versions:
+      - 'stream10'
+      - 'stream9'
diff --git a/.gitlab-ci/fedora-install.sh b/.gitlab-ci/fedora-install.sh
index 50f9710f..8f49d969 100755
--- a/.gitlab-ci/fedora-install.sh
+++ b/.gitlab-ci/fedora-install.sh
@@ -13,6 +13,8 @@ if [ $IS_CENTOS = 1 ]; then
         CENTOS_VERSION=8
     elif grep -q '^VERSION_ID=.*\<9\>' /etc/os-release ; then
         CENTOS_VERSION=9
+    elif grep -q '^VERSION_ID=.*\<10\>' /etc/os-release ; then
+        CENTOS_VERSION=10
     else
         exit 1
     fi
@@ -33,6 +35,11 @@ fi
         dnf install -y 'dnf-command(config-manager)'
         dnf config-manager --set-enabled crb
         curl https://copr.fedorainfracloud.org/coprs/nmstate/nm-build-deps/repo/epel-9/nmstate-nm-build-deps-epel-9.repo > /etc/yum.repos.d/nmstate-nm-build-deps-epel-9.repo
+    elif [ "$CENTOS_VERSION" = stream10 ]; then
+        dnf install -y https://dl.fedoraproject.org/pub/epel/epel-release-latest-10.noarch.rpm
+        dnf install -y 'dnf-command(config-manager)'
+        dnf config-manager --set-enabled crb
+        curl https://copr.fedorainfracloud.org/coprs/nmstate/nm-build-deps/repo/epel-10/nmstate-nm-build-deps-epel-10.repo > /etc/yum.repos.d/nmstate-nm-build-deps-epel-10.repo
     else
         exit 1
     fi
diff --git a/.gitlab-ci/run-test.sh b/.gitlab-ci/run-test.sh
index 265dbd54..44405558 100755
--- a/.gitlab-ci/run-test.sh
+++ b/.gitlab-ci/run-test.sh
@@ -17,10 +17,17 @@ grep -q '^NAME=.*\(CentOS\)' /etc/os-release && IS_CENTOS=1
 grep -q '^NAME=.*\(Fedora\)' /etc/os-release && IS_FEDORA=1
 grep -q '^NAME=.*\(Alpine\)' /etc/os-release && IS_ALPINE=1
 
-IS_CENTOS_7=0
 if [ $IS_CENTOS = 1 ]; then
-    if grep -q '^VERSION_ID=.*\<7\>' /etc/os-release ; then
-        IS_CENTOS_7=1
+    CENTOS_VER_LINE="$(grep '^VERSION_ID=' /etc/os-release)"
+    if [[ $CENTOS_VER_LINE =~ ^VERSION_ID=\"?([0-9]+)\"?$ ]]; then
+        CENTOS_VER="${BASH_REMATCH[1]}"
+    else
+        echo "Error detecting CentOS Stream version" >&2
+        exit 1
+    fi
+
+    if (( $CENTOS_VER >= 10 )); then
+        export WITH_LIBTEAM=0
     fi
 fi
 
diff --git a/NEWS b/NEWS
index 5b0a9813..6d2ab52a 100644
--- a/NEWS
+++ b/NEWS
@@ -1,3 +1,14 @@
+===============================================
+NetworkManager-1.54.1
+Overview of changes since NetworkManager-1.54.0
+===============================================
+
+* Make that global-dns configuration overwrites DNS searches and
+  options from connections, instead of merging all together.
+* Add support for a new rd.net.dhcp.client-id option in
+  nm-initrd-generator.
+* Minor bug fixes.
+
 =============================================
 NetworkManager-1.54
 Overview of changes since NetworkManager-1.52
diff --git a/contrib/alpine/REQUIRED_PACKAGES b/contrib/alpine/REQUIRED_PACKAGES
index 6f0e86fd..9f1affcb 100755
--- a/contrib/alpine/REQUIRED_PACKAGES
+++ b/contrib/alpine/REQUIRED_PACKAGES
@@ -11,6 +11,7 @@ apk add \
     'clang' \
     'curl-dev' \
     'dbus' \
+    'dbus-dev' \
     'elogind-dev' \
     'eudev-dev' \
     'gcc' \
diff --git a/contrib/fedora/rpm/NetworkManager.spec b/contrib/fedora/rpm/NetworkManager.spec
index 788e9ead..004cbbb3 100644
--- a/contrib/fedora/rpm/NetworkManager.spec
+++ b/contrib/fedora/rpm/NetworkManager.spec
@@ -691,7 +691,7 @@ Preferably use nmcli instead.
 	-Dsession_tracking=systemd \
 	-Dsuspend_resume=systemd \
 	-Dsystemdsystemunitdir=%{_unitdir} \
-	-Dsystem_ca_path=/etc/pki/tls/cert.pem \
+	-Dsystem_ca_path=/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem \
 	-Ddbus_conf_dir=%{dbus_sys_dir} \
 	-Dtests=yes \
 	-Dvalgrind=no \
diff --git a/contrib/scripts/nm-ci-run.sh b/contrib/scripts/nm-ci-run.sh
index 4881d110..2ae96a7c 100755
--- a/contrib/scripts/nm-ci-run.sh
+++ b/contrib/scripts/nm-ci-run.sh
@@ -84,6 +84,14 @@ if [ "$CC" != gcc ]; then
     _WITH_CRYPTO=nss
 fi
 
+if [ "$WITH_LIBTEAM" != "" ]; then
+    if _is_true "$WITH_LIBTEAM"; then
+        _WITH_LIBTEAM="true"
+    else
+        _WITH_LIBTEAM="false"
+    fi
+fi
+
 if [ "$WITH_DOCS" != "" ]; then
     if _is_true "$WITH_DOCS"; then
         _WITH_DOCS="true"
diff --git a/man/NetworkManager.conf.xml b/man/NetworkManager.conf.xml
index 2b427559..3f9aec79 100644
--- a/man/NetworkManager.conf.xml
+++ b/man/NetworkManager.conf.xml
@@ -1540,8 +1540,12 @@ managed=1
 
   <refsect1>
     <title><literal>global-dns</literal> section</title>
-    <para>This section specifies DNS settings that are applied
-    globally, in addition to connection-specific ones.</para>
+    <para>This section specifies DNS settings that are applied globally. They
+    override the equivalent options defined in individual connections, making
+    them to be ignored. If a [global-dns-domain-*] section is defined, but this
+    section isn't, an empty [global-dns] section is assumed, thus overwriting
+    connection specific configurations too.
+    </para>
     <para>
       <variablelist>
         <varlistentry>
@@ -1601,6 +1605,10 @@ managed=1
     default domain "*". When the global DNS domains are valid, the
     name servers and domains defined globally override the ones from
     active connections.
+
+    If any global DNS domain is defined but a [global-dns] section isn't,
+    an empty [global-dns] section is assumed, thus overwriting its
+    connection specific configurations too.
     </para>
     <para>
       <variablelist>
diff --git a/man/nm-initrd-generator.xml b/man/nm-initrd-generator.xml
index 312edff2..f9e254d7 100644
--- a/man/nm-initrd-generator.xml
+++ b/man/nm-initrd-generator.xml
@@ -162,6 +162,7 @@
             <member><option>rd.net.dns-backend</option></member>
             <member><option>rd.net.dns-resolve-mode</option></member>
             <member><option>rd.net.timeout.dhcp</option></member>
+            <member><option>rd.net.dhcp.client-id</option></member>
             <member><option>rd.net.dhcp.retry</option></member>
             <member><option>rd.net.dhcp.vendor-class</option></member>
             <member><option>rd.net.dhcp.dscp</option></member>
@@ -268,6 +269,23 @@
         </para>
       </listitem>
 
+      <listitem>
+        <para>NetworkManager supports the
+        <option>rd.net.dhcp.client-id</option>=<replaceable>interface</replaceable>:<replaceable>client-id</replaceable>
+        kernel command line option to set a specific DHCPv4 client identifier
+        for the given interface. The client-id can be specified either as a
+        sequence of bytes in hexadecimal format separated by dashes, or as the
+        character '@' followed by a non-empty string. When using the second
+        format, NetworkManager prepends a zero byte to the given string,
+        according to section 9.14 of RFC 2132. See the "ipv4.dhcp-client-id"
+        section of <link
+        linkend='nm-settings-nmcli'><citerefentry><refentrytitle>nm-settings-nmcli</refentrytitle><manvolnum>5</manvolnum></citerefentry></link>
+        for more details. Examples:
+        <literal>rd.net.dhcp.client-id=eth0:01-52-54-00-45-87-42</literal>,
+        <literal>rd.net.dhcp.client-id=enp1s0:@example.com</literal>.
+        </para>
+      </listitem>
+
     </itemizedlist>
   </refsect1>
 
@@ -278,6 +296,7 @@
 
   <refsect1 id='see_also'><title>See Also</title>
     <para><link linkend='dracut.cmdline'><citerefentry><refentrytitle>dracut.cmdline</refentrytitle><manvolnum>7</manvolnum></citerefentry></link>,
-    <link linkend='NetworkManager'><citerefentry><refentrytitle>NetworkManager</refentrytitle><manvolnum>8</manvolnum></citerefentry></link>.</para>
+    <link linkend='NetworkManager'><citerefentry><refentrytitle>NetworkManager</refentrytitle><manvolnum>8</manvolnum></citerefentry></link>,
+    <link linkend='nm-settings-nmcli'><citerefentry><refentrytitle>nm-settings-nmcli</refentrytitle><manvolnum>5</manvolnum></citerefentry></link>.</para>
   </refsect1>
 </refentry>
diff --git a/meson.build b/meson.build
index 2eac4ed7..c256823e 100644
--- a/meson.build
+++ b/meson.build
@@ -5,7 +5,7 @@ project(
 # NOTE: When incrementing version also add corresponding
 #       NM_VERSION_x_y_z macros in
 #       "src/libnm-core-public/nm-version-macros.h.in"
-  version: '1.54.0',
+  version: '1.54.1',
   license: 'GPL2+',
   default_options: [
     'buildtype=debugoptimized',
diff --git a/src/core/devices/nm-device-bond.c b/src/core/devices/nm-device-bond.c
index 673d2361..39e68e96 100644
--- a/src/core/devices/nm-device-bond.c
+++ b/src/core/devices/nm-device-bond.c
@@ -52,8 +52,7 @@
         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_LACP_ACTIVE
+        NM_SETTING_BOND_OPTION_PEER_NOTIF_DELAY, NM_SETTING_BOND_OPTION_ARP_MISSED_MAX
 
 #define OPTIONS_REAPPLY_FULL                                     \
     OPTIONS_REAPPLY_SUBSET, NM_SETTING_BOND_OPTION_ACTIVE_SLAVE, \
diff --git a/src/core/devices/nm-device.c b/src/core/devices/nm-device.c
index f6057e52..79b1076d 100644
--- a/src/core/devices/nm-device.c
+++ b/src/core/devices/nm-device.c
@@ -9164,6 +9164,10 @@ is_available(NMDevice *self, NMDeviceCheckDevAvailableFlags flags)
 {
     NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self);
 
+    /* unrealized software devices are always available, hardware devices never */
+    if (!nm_device_is_real(self))
+        return nm_device_is_software(self);
+
     if (priv->carrier || priv->ignore_carrier)
         return TRUE;
 
@@ -14696,13 +14700,15 @@ impl_device_delete(NMDBusObject                      *obj,
                            NULL);
 }
 
-static void
+static gboolean
 _device_activate(NMDevice *self, NMActRequest *req)
 {
     NMConnection *connection;
 
-    g_return_if_fail(NM_IS_DEVICE(self));
-    g_return_if_fail(NM_IS_ACT_REQUEST(req));
+    /* Returns TRUE on success, FALSE if the activation request could not be started */
+
+    g_return_val_if_fail(NM_IS_DEVICE(self), FALSE);
+    g_return_val_if_fail(NM_IS_ACT_REQUEST(req), FALSE);
     nm_assert(nm_device_is_real(self));
 
     /* Ensure the activation request is still valid; the controller may have
@@ -14710,7 +14716,7 @@ _device_activate(NMDevice *self, NMActRequest *req)
      */
     if (nm_active_connection_get_state(NM_ACTIVE_CONNECTION(req))
         >= NM_ACTIVE_CONNECTION_STATE_DEACTIVATING)
-        return;
+        return FALSE;
 
     if (!nm_device_get_managed(self, FALSE)) {
         /* It's unclear why the device would be unmanaged at this point.
@@ -14721,7 +14727,7 @@ _device_activate(NMDevice *self, NMActRequest *req)
         nm_active_connection_set_state_fail((NMActiveConnection *) req,
                                             NM_ACTIVE_CONNECTION_STATE_REASON_UNKNOWN,
                                             NULL);
-        return;
+        return FALSE;
     }
 
     connection = nm_act_request_get_applied_connection(req);
@@ -14737,6 +14743,8 @@ _device_activate(NMDevice *self, NMActRequest *req)
     act_request_set(self, req);
 
     nm_device_activate_schedule_stage1_device_prepare(self, FALSE);
+
+    return TRUE;
 }
 
 static void
@@ -14756,7 +14764,9 @@ _carrier_wait_check_queued_act_request(NMDevice *self)
 
         _LOGD(LOGD_DEVICE, "Activate queued activation request as we now have carrier");
         queued_req = g_steal_pointer(&priv->queued_act_request);
-        _device_activate(self, queued_req);
+        if (!_device_activate(self, queued_req)) {
+            delete_on_deactivate_check_and_schedule(self);
+        }
     }
 }
 
@@ -17534,7 +17544,8 @@ _set_state_full(NMDevice *self, NMDeviceState state, NMDeviceStateReason reason,
             gs_unref_object NMActRequest *queued_req = NULL;
 
             queued_req = g_steal_pointer(&priv->queued_act_request);
-            _device_activate(self, queued_req);
+            if (!_device_activate(self, queued_req))
+                delete_on_deactivate_check_and_schedule(self);
         }
         break;
     case NM_DEVICE_STATE_ACTIVATED:
diff --git a/src/core/devices/wifi/nm-device-wifi.c b/src/core/devices/wifi/nm-device-wifi.c
index 06eee142..b890b110 100644
--- a/src/core/devices/wifi/nm-device-wifi.c
+++ b/src/core/devices/wifi/nm-device-wifi.c
@@ -196,7 +196,8 @@ static void periodic_update(NMDeviceWifi *self);
 static void ap_add_remove(NMDeviceWifi *self,
                           gboolean      is_adding,
                           NMWifiAP     *ap,
-                          gboolean      recheck_available_connections);
+                          gboolean      recheck_available_connections,
+                          gboolean      recheck_auto_activate);
 
 static void _hw_addr_set_scanning(NMDeviceWifi *self, gboolean do_reset);
 
@@ -714,7 +715,10 @@ update_seen_bssids_cache(NMDeviceWifi *self, NMWifiAP *ap)
 }
 
 static void
-set_current_ap(NMDeviceWifi *self, NMWifiAP *new_ap, gboolean recheck_available_connections)
+set_current_ap(NMDeviceWifi *self,
+               NMWifiAP     *new_ap,
+               gboolean      recheck_available_connections,
+               gboolean      recheck_auto_activate)
 {
     NMDeviceWifiPrivate *priv;
     NMWifiAP            *old_ap;
@@ -741,7 +745,11 @@ set_current_ap(NMDeviceWifi *self, NMWifiAP *new_ap, gboolean recheck_available_
         /* Remove any AP from the internal list if it was created by NM or isn't known to the supplicant */
         if (NM_IN_SET(mode, _NM_802_11_MODE_ADHOC, _NM_802_11_MODE_AP)
             || nm_wifi_ap_get_fake(old_ap))
-            ap_add_remove(self, FALSE, old_ap, recheck_available_connections);
+            ap_add_remove(self,
+                          FALSE,
+                          old_ap,
+                          recheck_available_connections,
+                          recheck_auto_activate);
         g_object_unref(old_ap);
     }
 
@@ -814,7 +822,8 @@ static void
 ap_add_remove(NMDeviceWifi *self,
               gboolean      is_adding, /* or else removing */
               NMWifiAP     *ap,
-              gboolean      recheck_available_connections)
+              gboolean      recheck_available_connections,
+              gboolean      recheck_auto_activate)
 {
     NMDeviceWifiPrivate *priv = NM_DEVICE_WIFI_GET_PRIVATE(self);
 
@@ -845,13 +854,14 @@ ap_add_remove(NMDeviceWifi *self,
         nm_dbus_object_clear_and_unexport(&ap);
     }
 
-    nm_device_recheck_auto_activate_schedule(NM_DEVICE(self));
+    if (recheck_auto_activate)
+        nm_device_recheck_auto_activate_schedule(NM_DEVICE(self));
     if (recheck_available_connections)
         nm_device_recheck_available_connections(NM_DEVICE(self));
 }
 
 static void
-remove_all_aps(NMDeviceWifi *self)
+remove_all_aps(NMDeviceWifi *self, gboolean disposing)
 {
     NMDeviceWifiPrivate *priv = NM_DEVICE_WIFI_GET_PRIVATE(self);
     NMWifiAP            *ap;
@@ -859,12 +869,13 @@ remove_all_aps(NMDeviceWifi *self)
     if (c_list_is_empty(&priv->aps_lst_head))
         return;
 
-    set_current_ap(self, NULL, FALSE);
+    set_current_ap(self, NULL, FALSE, !disposing);
 
     while ((ap = c_list_first_entry(&priv->aps_lst_head, NMWifiAP, aps_lst)))
-        ap_add_remove(self, FALSE, ap, FALSE);
+        ap_add_remove(self, FALSE, ap, FALSE, !disposing);
 
-    nm_device_recheck_available_connections(NM_DEVICE(self));
+    if (!disposing)
+        nm_device_recheck_available_connections(NM_DEVICE(self));
 }
 
 static gboolean
@@ -951,7 +962,7 @@ deactivate(NMDevice *device)
 
     priv->rate = 0;
 
-    set_current_ap(self, NULL, TRUE);
+    set_current_ap(self, NULL, TRUE, TRUE);
 
     if (!wake_on_wlan_restore(self))
         _LOGW(LOGD_DEVICE | LOGD_WIFI, "Cannot unconfigure WoWLAN.");
@@ -2000,7 +2011,7 @@ supplicant_iface_bss_changed_cb(NMSupplicantInterface *iface,
             if (nm_wifi_ap_set_fake(found_ap, TRUE))
                 _ap_dump(self, LOGL_DEBUG, found_ap, "updated", 0);
         } else {
-            ap_add_remove(self, FALSE, found_ap, TRUE);
+            ap_add_remove(self, FALSE, found_ap, TRUE, TRUE);
             schedule_ap_list_dump(self);
         }
         return;
@@ -2043,7 +2054,7 @@ supplicant_iface_bss_changed_cb(NMSupplicantInterface *iface,
             }
         }
 
-        ap_add_remove(self, TRUE, ap, TRUE);
+        ap_add_remove(self, TRUE, ap, TRUE, TRUE);
     }
 
     /* Update the current AP if the supplicant notified a current BSS change
@@ -2268,7 +2279,7 @@ link_timeout_cb(gpointer user_data)
     if (nm_device_get_state(device) != NM_DEVICE_STATE_ACTIVATED)
         return FALSE;
 
-    set_current_ap(self, NULL, TRUE);
+    set_current_ap(self, NULL, TRUE, TRUE);
 
     nm_device_state_changed(device,
                             NM_DEVICE_STATE_FAILED,
@@ -2684,7 +2695,7 @@ supplicant_iface_notify_current_bss(NMSupplicantInterface *iface,
             }
         }
 
-        set_current_ap(self, new_ap, TRUE);
+        set_current_ap(self, new_ap, TRUE, TRUE);
 
         req = nm_device_get_act_request(NM_DEVICE(self));
         if (req) {
@@ -3118,7 +3129,7 @@ act_stage1_prepare(NMDevice *device, NMDeviceStateReason *out_failure_reason)
         priv->mode = _NM_802_11_MODE_AP;
 
         /* Scanning not done in AP mode; clear the scan list */
-        remove_all_aps(self);
+        remove_all_aps(self, FALSE);
     } else if (g_strcmp0(mode, NM_SETTING_WIRELESS_MODE_MESH) == 0)
         priv->mode = _NM_802_11_MODE_MESH;
     _notify(self, PROP_MODE);
@@ -3155,14 +3166,14 @@ act_stage1_prepare(NMDevice *device, NMDeviceStateReason *out_failure_reason)
             nm_wifi_ap_set_address(ap_fake, nm_device_get_hw_address(device));
 
         g_object_freeze_notify(G_OBJECT(self));
-        ap_add_remove(self, TRUE, ap_fake, TRUE);
+        ap_add_remove(self, TRUE, ap_fake, TRUE, TRUE);
         g_object_thaw_notify(G_OBJECT(self));
         ap = ap_fake;
     }
 
     _scan_notify_allowed(self, NM_TERNARY_DEFAULT);
 
-    set_current_ap(self, ap, FALSE);
+    set_current_ap(self, ap, FALSE, TRUE);
     nm_active_connection_set_specific_object(NM_ACTIVE_CONNECTION(req),
                                              nm_dbus_object_get_path(NM_DBUS_OBJECT(ap)));
     return NM_ACT_STAGE_RETURN_SUCCESS;
@@ -3528,7 +3539,7 @@ device_state_changed(NMDevice           *device,
 
         cleanup_association_attempt(self, TRUE);
         cleanup_supplicant_failures(self);
-        remove_all_aps(self);
+        remove_all_aps(self, FALSE);
     }
 
     switch (new_state) {
@@ -3566,7 +3577,7 @@ device_state_changed(NMDevice           *device,
     }
 
     if (clear_aps)
-        remove_all_aps(self);
+        remove_all_aps(self, FALSE);
 
     _scan_notify_allowed(self, NM_TERNARY_DEFAULT);
 }
@@ -3808,7 +3819,7 @@ dispose(GObject *object)
 
     g_clear_object(&priv->sup_mgr);
 
-    remove_all_aps(self);
+    remove_all_aps(self, TRUE);
 
     if (priv->p2p_device) {
         /* Destroy the P2P device. */
diff --git a/src/core/dns/nm-dns-manager.c b/src/core/dns/nm-dns-manager.c
index d47590dc..57e73226 100644
--- a/src/core/dns/nm-dns-manager.c
+++ b/src/core/dns/nm-dns-manager.c
@@ -586,7 +586,11 @@ add_dns_domains(GPtrArray            *array,
 }
 
 static void
-merge_one_l3cd(NMResolvConfData *rc, int addr_family, int ifindex, const NML3ConfigData *l3cd)
+merge_one_l3cd(NMResolvConfData     *rc,
+               int                   addr_family,
+               int                   ifindex,
+               const NML3ConfigData *l3cd,
+               gboolean              ignore_searches_and_options)
 {
     char               buf[NM_INET_ADDRSTRLEN + 50];
     gboolean           has_trust_ad;
@@ -624,30 +628,32 @@ merge_one_l3cd(NMResolvConfData *rc, int addr_family, int ifindex, const NML3Con
         add_string_item(rc->nameservers, buf, TRUE);
     }
 
-    add_dns_domains(rc->searches, addr_family, l3cd, FALSE, TRUE);
+    if (!ignore_searches_and_options) {
+        add_dns_domains(rc->searches, addr_family, l3cd, FALSE, TRUE);
 
-    has_trust_ad = FALSE;
-    strarr       = nm_l3_config_data_get_dns_options(l3cd, addr_family, &num);
-    for (i = 0; i < num; i++) {
-        const char *option = strarr[i];
+        has_trust_ad = FALSE;
+        strarr       = nm_l3_config_data_get_dns_options(l3cd, addr_family, &num);
+        for (i = 0; i < num; i++) {
+            const char *option = strarr[i];
 
-        if (nm_streq(option, NM_SETTING_DNS_OPTION_TRUST_AD)) {
-            has_trust_ad = TRUE;
-            continue;
+            if (nm_streq(option, NM_SETTING_DNS_OPTION_TRUST_AD)) {
+                has_trust_ad = TRUE;
+                continue;
+            }
+            add_dns_option_item(rc->options, option);
         }
-        add_dns_option_item(rc->options, option);
-    }
 
-    if (num_nameservers == 0) {
-        /* If the @l3cd contributes no DNS servers, ignore whether trust-ad is set or unset
-         * for this @l3cd. */
-    } else if (has_trust_ad) {
-        /* We only set has_trust_ad to TRUE, if all IP configs agree (or don't contribute).
-         * Once set to FALSE, it doesn't get reset. */
-        if (rc->has_trust_ad == NM_TERNARY_DEFAULT)
-            rc->has_trust_ad = NM_TERNARY_TRUE;
-    } else
-        rc->has_trust_ad = NM_TERNARY_FALSE;
+        if (num_nameservers == 0) {
+            /* If the @l3cd contributes no DNS servers, ignore whether trust-ad is set or unset
+             * for this @l3cd. */
+        } else if (has_trust_ad) {
+            /* We only set has_trust_ad to TRUE, if all IP configs agree (or don't contribute).
+             * Once set to FALSE, it doesn't get reset. */
+            if (rc->has_trust_ad == NM_TERNARY_DEFAULT)
+                rc->has_trust_ad = NM_TERNARY_TRUE;
+        } else
+            rc->has_trust_ad = NM_TERNARY_FALSE;
+    }
 
     if (addr_family == AF_INET) {
         const in_addr_t *nis_servers;
@@ -1231,12 +1237,15 @@ compute_hash(NMDnsManager *self, const NMGlobalDnsConfig *global, guint8 buffer[
 {
     nm_auto_free_checksum GChecksum *sum = NULL;
     NMDnsConfigIPData               *ip_data;
+    gboolean                         has_global_dns_section = FALSE;
 
     sum = g_checksum_new(G_CHECKSUM_SHA1);
     nm_assert(HASH_LEN == g_checksum_type_get_length(G_CHECKSUM_SHA1));
 
-    if (global)
+    if (global) {
         nm_global_dns_config_update_checksum(global, sum);
+        has_global_dns_section = nm_global_dns_has_global_dns_section(global);
+    }
 
     if (!global || !nm_global_dns_config_lookup_domain(global, "*")) {
         const CList *head;
@@ -1248,7 +1257,8 @@ compute_hash(NMDnsManager *self, const NMGlobalDnsConfig *global, guint8 buffer[
             nm_l3_config_data_hash_dns(ip_data->l3cd,
                                        sum,
                                        ip_data->addr_family,
-                                       ip_data->ip_config_type);
+                                       ip_data->ip_config_type,
+                                       has_global_dns_section);
         }
     }
 
@@ -1264,6 +1274,9 @@ merge_global_dns_config(NMResolvConfData *rc, NMGlobalDnsConfig *global_conf)
     const char *const *servers;
     guint              i;
 
+    /* Global config must be processed before connections' config */
+    nm_assert(rc->nameservers->len == 0);
+
     if (!global_conf)
         return FALSE;
 
@@ -1351,12 +1364,17 @@ _collect_resolv_conf_data(NMDnsManager      *self,
             .nis_servers  = g_ptr_array_new(),
             .has_trust_ad = NM_TERNARY_DEFAULT,
     };
+    gboolean has_global_dns_section = FALSE;
 
     priv = NM_DNS_MANAGER_GET_PRIVATE(self);
 
-    if (global_config)
+    if (global_config) {
         merge_global_dns_config(&rc, global_config);
+        has_global_dns_section = nm_global_dns_has_global_dns_section(global_config);
+    }
 
+    /* If global nameservers are defined, no DNS configs are used from connections at all,
+     * including searches and options. */
     if (!global_config || !nm_global_dns_config_lookup_domain(global_config, "*")) {
         nm_auto_str_buf NMStrBuf tmp_strbuf = NM_STR_BUF_INIT(0, FALSE);
         int                      first_prio = 0;
@@ -1390,8 +1408,16 @@ _collect_resolv_conf_data(NMDnsManager      *self,
                   skip ? "<SKIP>" : "",
                   get_nameserver_list(ip_data->addr_family, ip_data->l3cd, &tmp_strbuf));
 
-            if (!skip)
-                merge_one_l3cd(&rc, ip_data->addr_family, ip_data->data->ifindex, ip_data->l3cd);
+            if (!skip) {
+                /* Merge the configs from connections. However, if there was a [global-dns]
+                 * it overwrites searches and options from the connections, thus we only
+                 * merge the nameservers. */
+                merge_one_l3cd(&rc,
+                               ip_data->addr_family,
+                               ip_data->data->ifindex,
+                               ip_data->l3cd,
+                               has_global_dns_section);
+            }
         }
     }
 
diff --git a/src/core/nm-config-data.c b/src/core/nm-config-data.c
index 5a84a2c8..461fd8ed 100644
--- a/src/core/nm-config-data.c
+++ b/src/core/nm-config-data.c
@@ -50,9 +50,9 @@ struct _NMGlobalDnsConfig {
     char           **options;
     GHashTable      *domains;
     const char     **domain_list;
-    gboolean         internal;
     char            *cert_authority;
     NMDnsResolveMode resolve_mode;
+    gboolean         internal;
 };
 
 /*****************************************************************************/
@@ -941,6 +941,14 @@ next:
 
 /*****************************************************************************/
 
+gboolean
+nm_global_dns_has_global_dns_section(const NMGlobalDnsConfig *dns_config)
+{
+    g_return_val_if_fail(dns_config, FALSE);
+
+    return dns_config->searches != NULL || dns_config->options != NULL;
+}
+
 const char *const *
 nm_global_dns_config_get_searches(const NMGlobalDnsConfig *dns_config)
 {
@@ -1236,6 +1244,7 @@ load_global_dns(GKeyFile *keyfile, gboolean internal)
     gs_free char      *cert_authority = NULL;
     gs_free char      *resolve_mode   = NULL;
     NMDnsResolveMode   parsed_resolve_mode;
+    gboolean           has_global_dns_section;
 
     if (internal) {
         group         = NM_CONFIG_KEYFILE_GROUP_INTERN_GLOBAL_DNS;
@@ -1386,6 +1395,22 @@ load_global_dns(GKeyFile *keyfile, gboolean internal)
         return NULL;
     }
 
+    /* Defining [global-dns-domain-*] implies defining [global-dns] too (maybe empty) */
+    if (default_found)
+        has_global_dns_section = TRUE;
+    else
+        has_global_dns_section = g_key_file_has_group(keyfile, group);
+
+    /* If there exist a [global-dns] section, always initialize "searches" and "options" so
+     * they appear in D-Bus. Clients can use this to know if it's defined, so they can know
+     * if DNS configs from connections are relevant or not. */
+    if (has_global_dns_section) {
+        if (!dns_config->searches)
+            dns_config->searches = nm_strv_empty_new();
+        if (!dns_config->options)
+            dns_config->options = nm_strv_empty_new();
+    }
+
     dns_config->internal = internal;
     global_dns_config_seal_domains(dns_config);
     return dns_config;
@@ -1606,17 +1631,6 @@ nm_global_dns_config_from_dbus(const GValue *value, GError **error)
         g_variant_unref(val);
     }
 
-    /* An empty value is valid and clears the internal configuration */
-    if (!nm_global_dns_config_is_empty(dns_config)
-        && !nm_global_dns_config_lookup_domain(dns_config, "*")) {
-        g_set_error_literal(error,
-                            NM_MANAGER_ERROR,
-                            NM_MANAGER_ERROR_FAILED,
-                            "Global DNS configuration is missing the default domain");
-        nm_global_dns_config_free(dns_config);
-        return NULL;
-    }
-
     global_dns_config_seal_domains(dns_config);
     return dns_config;
 }
diff --git a/src/core/nm-config-data.h b/src/core/nm-config-data.h
index d764a7c4..6666ccf6 100644
--- a/src/core/nm-config-data.h
+++ b/src/core/nm-config-data.h
@@ -274,6 +274,7 @@ gboolean nm_config_data_is_intern_atomic_group(const NMConfigData *self, const c
 
 GKeyFile *nm_config_data_clone_keyfile_intern(const NMConfigData *self);
 
+gboolean           nm_global_dns_has_global_dns_section(const NMGlobalDnsConfig *dns_config);
 const char *const *nm_global_dns_config_get_searches(const NMGlobalDnsConfig *dns_config);
 const char *const *nm_global_dns_config_get_options(const NMGlobalDnsConfig *dns_config);
 const char *nm_global_dns_config_get_certification_authority(const NMGlobalDnsConfig *dns_config);
diff --git a/src/core/nm-config.c b/src/core/nm-config.c
index a55d2d13..d1f2bbed 100644
--- a/src/core/nm-config.c
+++ b/src/core/nm-config.c
@@ -18,6 +18,7 @@
 #include "libnm-core-intern/nm-core-internal.h"
 #include "libnm-core-intern/nm-keyfile-internal.h"
 #include "libnm-core-intern/nm-keyfile-utils.h"
+#include "libnm-glib-aux/nm-keyfile-aux.h"
 
 #define DEFAULT_CONFIG_MAIN_FILE     NMCONFDIR "/NetworkManager.conf"
 #define DEFAULT_CONFIG_DIR           NMCONFDIR "/conf.d"
@@ -1046,6 +1047,10 @@ read_config(GKeyFile   *keyfile,
             /* internal groups cannot be set by user configuration. */
             continue;
         }
+
+        if (!g_key_file_has_group(keyfile, group))
+            nm_key_file_add_group(keyfile, group);
+
         keys = g_key_file_get_keys(kf, group, &nkeys, NULL);
         if (!keys)
             continue;
@@ -1639,6 +1644,12 @@ intern_config_read(const char        *filename,
                                  "");
         }
 
+        if (!g_key_file_has_group(keyfile_intern, group)) {
+            nm_key_file_add_group(keyfile_intern, group);
+            if (is_intern)
+                has_intern = TRUE;
+        }
+
         for (k = 0; keys[k]; k++) {
             gs_free char *value_set = NULL;
             const char   *key       = keys[k];
@@ -1823,6 +1834,9 @@ intern_config_write(const char        *filename,
             }
         }
 
+        if (!g_key_file_has_group(keyfile, group))
+            nm_key_file_add_group(keyfile, group);
+
         for (k = 0; keys[k]; k++) {
             const char   *key       = keys[k];
             gs_free char *value_set = NULL;
diff --git a/src/core/nm-l3-config-data.c b/src/core/nm-l3-config-data.c
index 20623145..666aa8a3 100644
--- a/src/core/nm-l3-config-data.c
+++ b/src/core/nm-l3-config-data.c
@@ -3139,7 +3139,8 @@ void
 nm_l3_config_data_hash_dns(const NML3ConfigData *l3cd,
                            GChecksum            *sum,
                            int                   addr_family,
-                           NMDnsIPConfigType     dns_ip_config_type)
+                           NMDnsIPConfigType     dns_ip_config_type,
+                           gboolean              ignore_searches_and_options)
 {
     guint              i;
     int                val;
@@ -3178,16 +3179,18 @@ nm_l3_config_data_hash_dns(const NML3ConfigData *l3cd,
         empty = FALSE;
     }
 
-    searches = nm_l3_config_data_get_searches(l3cd, addr_family, &num_searches);
-    for (i = 0; i < num_searches; i++) {
-        g_checksum_update(sum, (const guint8 *) searches[i], strlen(searches[i]));
-        empty = FALSE;
-    }
+    if (!ignore_searches_and_options) {
+        searches = nm_l3_config_data_get_searches(l3cd, addr_family, &num_searches);
+        for (i = 0; i < num_searches; i++) {
+            g_checksum_update(sum, (const guint8 *) searches[i], strlen(searches[i]));
+            empty = FALSE;
+        }
 
-    options = nm_l3_config_data_get_dns_options(l3cd, addr_family, &num_options);
-    for (i = 0; i < num_options; i++) {
-        g_checksum_update(sum, (const guint8 *) options[i], strlen(options[i]));
-        empty = FALSE;
+        options = nm_l3_config_data_get_dns_options(l3cd, addr_family, &num_options);
+        for (i = 0; i < num_options; i++) {
+            g_checksum_update(sum, (const guint8 *) options[i], strlen(options[i]));
+            empty = FALSE;
+        }
     }
 
     val = nm_l3_config_data_get_mdns(l3cd);
diff --git a/src/core/nm-l3-config-data.h b/src/core/nm-l3-config-data.h
index bcb6af67..265e126d 100644
--- a/src/core/nm-l3-config-data.h
+++ b/src/core/nm-l3-config-data.h
@@ -615,6 +615,7 @@ nmtst_l3_config_data_get_best_gateway(const NML3ConfigData *self, int addr_famil
 void nm_l3_config_data_hash_dns(const NML3ConfigData *l3cd,
                                 GChecksum            *sum,
                                 int                   addr_family,
-                                NMDnsIPConfigType     dns_ip_config_type);
+                                NMDnsIPConfigType     dns_ip_config_type,
+                                gboolean              ignore_searches_and_options);
 
 #endif /* __NM_L3_CONFIG_DATA_H__ */
diff --git a/src/core/nm-manager.c b/src/core/nm-manager.c
index fce37071..9dc7bc01 100644
--- a/src/core/nm-manager.c
+++ b/src/core/nm-manager.c
@@ -5718,6 +5718,7 @@ _internal_activate_device(NMManager *self, NMActiveConnection *active, GError **
     GError                  *local = NULL;
     NMConnectionMultiConnect multi_connect;
     const char              *parent_spec;
+    gboolean                 did_realize = FALSE;
 
     g_return_val_if_fail(NM_IS_MANAGER(self), FALSE);
     g_return_val_if_fail(NM_IS_ACTIVE_CONNECTION(active), FALSE);
@@ -5892,6 +5893,7 @@ _internal_activate_device(NMManager *self, NMActiveConnection *active, GError **
                                nm_device_get_iface(device));
                 return FALSE;
             }
+            did_realize = TRUE;
         }
     }
 
@@ -5923,7 +5925,7 @@ _internal_activate_device(NMManager *self, NMActiveConnection *active, GError **
                         "The controller connection '%s' is not compatible with '%s'",
                         nm_settings_connection_get_id(controller_connection),
                         nm_settings_connection_get_id(sett_conn));
-            return FALSE;
+            goto err_unrealize;
         }
 
         if (!controller_ac) {
@@ -5946,7 +5948,7 @@ _internal_activate_device(NMManager *self, NMActiveConnection *active, GError **
                                    "Controller connection '%s' can't be activated: ",
                                    nm_settings_connection_get_id(controller_connection));
                 }
-                return FALSE;
+                goto err_unrealize;
             }
         }
 
@@ -6040,7 +6042,7 @@ _internal_activate_device(NMManager *self, NMActiveConnection *active, GError **
                                 NM_MANAGER_ERROR,
                                 NM_MANAGER_ERROR_DEPENDENCY_FAILED,
                                 "Activation failed because the device is unmanaged");
-            return FALSE;
+            goto err_unrealize;
         }
     }
 
@@ -6048,6 +6050,11 @@ _internal_activate_device(NMManager *self, NMActiveConnection *active, GError **
     active_connection_add(self, active);
     nm_device_queue_activation(device, NM_ACT_REQUEST(active));
     return TRUE;
+
+err_unrealize:
+    if (did_realize)
+        nm_device_unrealize(device, TRUE, NULL);
+    return FALSE;
 }
 
 static gboolean
diff --git a/src/core/tests/config/global-dns-empty.conf b/src/core/tests/config/global-dns-empty.conf
new file mode 100644
index 00000000..fba823bf
--- /dev/null
+++ b/src/core/tests/config/global-dns-empty.conf
@@ -0,0 +1,3 @@
+# Good configuration, an empty global-dns section must be valid
+
+[global-dns]
diff --git a/src/core/tests/config/global-dns-not-set.conf b/src/core/tests/config/global-dns-not-set.conf
new file mode 100644
index 00000000..133bd4ed
--- /dev/null
+++ b/src/core/tests/config/global-dns-not-set.conf
@@ -0,0 +1,5 @@
+# Good configuration, an empty [global-dns] must be implicitly assumed because a domain is defined
+
+[global-dns-domain-*]
+servers=4.5.6.7
+options=myoption1
diff --git a/src/core/tests/config/test-config.c b/src/core/tests/config/test-config.c
index 2980cda7..78fd1057 100644
--- a/src/core/tests/config/test-config.c
+++ b/src/core/tests/config/test-config.c
@@ -387,7 +387,27 @@ test_config_global_dns(void)
     g_assert(dns);
     g_object_unref(config);
 
-    /* Check that a file with a domain domain, but without a default one gives a NULL configuration */
+    /* Check that a file with an empty global-dns section gives a good configuration.
+     * Check also that searches and options are not NULL, as this is how we expose to
+     * D-Bus that global-dns is defined. */
+    config =
+        setup_config(NULL, TEST_DIR "/global-dns-empty.conf", "", NULL, "/no/such/dir", "", NULL);
+    dns = nm_config_data_get_global_dns_config(nm_config_get_data_orig(config));
+    g_assert(dns);
+    g_assert(nm_global_dns_config_get_searches(dns));
+    g_assert(nm_global_dns_config_get_options(dns));
+    g_object_unref(config);
+
+    /* Check that a file with a domain, but no global-dns, assumes an implicit empty global-dns */
+    config =
+        setup_config(NULL, TEST_DIR "/global-dns-not-set.conf", "", NULL, "/no/such/dir", "", NULL);
+    dns = nm_config_data_get_global_dns_config(nm_config_get_data_orig(config));
+    g_assert(dns);
+    g_assert(nm_global_dns_config_get_searches(dns));
+    g_assert(nm_global_dns_config_get_options(dns));
+    g_object_unref(config);
+
+    /* Check that a file with a domain, but without a default one, gives a NULL configuration */
     config =
         setup_config(NULL, TEST_DIR "/global-dns-invalid.conf", "", NULL, "/no/such/dir", "", NULL);
     dns = nm_config_data_get_global_dns_config(nm_config_get_data_orig(config));
diff --git a/src/libnm-client-impl/tests/test-nm-client.c b/src/libnm-client-impl/tests/test-nm-client.c
index 39831509..da252549 100644
--- a/src/libnm-client-impl/tests/test-nm-client.c
+++ b/src/libnm-client-impl/tests/test-nm-client.c
@@ -1432,13 +1432,13 @@ test_client_wait_shutdown(void)
     nmtst_assert_success(dbus_connection, error);
 
     for (i_run = 0; i_run < N_RUN; i_run++) {
-        gs_unref_object GCancellable          *init_cancellable = g_cancellable_new();
-        gs_unref_object NMClient              *nmc              = NULL;
-        nm_auto_pop_gmaincontext GMainContext *client_context   = NULL;
-        gboolean                               b;
-        gboolean                               context_integrated = FALSE;
-        gs_unref_object GCancellable          *cancellable_1      = NULL;
-        GMainContext                          *ctx;
+        gs_unref_object GCancellable                    *init_cancellable = g_cancellable_new();
+        gs_unref_object NMClient                        *nmc              = NULL;
+        nm_auto_pop_and_unref_gmaincontext GMainContext *client_context   = NULL;
+        gboolean                                         b;
+        gboolean                                         context_integrated = FALSE;
+        gs_unref_object GCancellable                    *cancellable_1      = NULL;
+        GMainContext                                    *ctx;
 
         /* Choose a random context for the client. */
         ctx = contexts->pdata[nmtst_get_rand_uint32() % contexts->len];
diff --git a/src/libnm-core-impl/nm-setting-wireguard.c b/src/libnm-core-impl/nm-setting-wireguard.c
index 668af1f6..614be0f6 100644
--- a/src/libnm-core-impl/nm-setting-wireguard.c
+++ b/src/libnm-core-impl/nm-setting-wireguard.c
@@ -1477,6 +1477,7 @@ peers_to_dbus(_NM_SETT_INFO_PROP_TO_DBUS_FCN_ARGS _nm_nil)
     for (i_peer = 0; i_peer < n_peers; i_peer++) {
         const NMWireGuardPeer *peer = _peers_get(priv, i_peer)->peer;
         GVariantBuilder        builder;
+        gboolean               has_secrets = FALSE;
 
         if (!peer->public_key)
             continue;
@@ -1496,11 +1497,13 @@ peers_to_dbus(_NM_SETT_INFO_PROP_TO_DBUS_FCN_ARGS _nm_nil)
                 g_variant_new_string(nm_sock_addr_endpoint_get_endpoint(peer->endpoint)));
 
         if (_nm_connection_serialize_secrets(flags, peer->preshared_key_flags)
-            && peer->preshared_key)
+            && peer->preshared_key) {
             g_variant_builder_add(&builder,
                                   "{sv}",
                                   NM_WIREGUARD_PEER_ATTR_PRESHARED_KEY,
                                   g_variant_new_string(peer->preshared_key));
+            has_secrets = TRUE;
+        }
 
         if (_nm_connection_serialize_non_secret(flags)
             && peer->preshared_key_flags != NM_SETTING_SECRET_FLAG_NOT_REQUIRED)
@@ -1546,6 +1549,20 @@ peers_to_dbus(_NM_SETT_INFO_PROP_TO_DBUS_FCN_ARGS _nm_nil)
                                   g_variant_new_strv(strv, peer->allowed_ips->len));
         }
 
+        if (NM_FLAGS_ANY(flags,
+                         NM_CONNECTION_SERIALIZE_ONLY_SECRETS
+                             | NM_CONNECTION_SERIALIZE_WITH_SECRETS_AGENT_OWNED
+                             | NM_CONNECTION_SERIALIZE_WITH_SECRETS_SYSTEM_OWNED
+                             | NM_CONNECTION_SERIALIZE_WITH_SECRETS_NOT_SAVED)
+            && !_nm_connection_serialize_non_secret(flags)) {
+            /* The flags indicate that only secrets must be serialized and this
+             * peer doesn't contain any. Skip the peer. */
+            if (!has_secrets) {
+                g_variant_builder_clear(&builder);
+                continue;
+            }
+        }
+
         if (!any_peers) {
             g_variant_builder_init(&peers_builder, G_VARIANT_TYPE("aa{sv}"));
             any_peers = TRUE;
diff --git a/src/libnm-core-impl/tests/test-setting.c b/src/libnm-core-impl/tests/test-setting.c
index 2064162a..13d95189 100644
--- a/src/libnm-core-impl/tests/test-setting.c
+++ b/src/libnm-core-impl/tests/test-setting.c
@@ -5522,6 +5522,223 @@ test_bond_meta(void)
 
 /*****************************************************************************/
 
+static void
+check_wg_setting_str(NMSetting  *s_wg,
+                     const char *exp_all,
+                     const char *exp_nonsec,
+                     const char *exp_sec)
+{
+    gs_unref_variant GVariant *dict_all    = NULL;
+    gs_unref_variant GVariant *dict_nonsec = NULL;
+    gs_unref_variant GVariant *dict_sec    = NULL;
+    gs_free char              *str_all     = NULL;
+    gs_free char              *str_nonsec  = NULL;
+    gs_free char              *str_sec     = NULL;
+
+    dict_all    = _nm_setting_to_dbus(s_wg, NULL, NM_CONNECTION_SERIALIZE_ALL, NULL);
+    dict_nonsec = _nm_setting_to_dbus(s_wg, NULL, NM_CONNECTION_SERIALIZE_WITH_NON_SECRET, NULL);
+    dict_sec    = _nm_setting_to_dbus(s_wg, NULL, NM_CONNECTION_SERIALIZE_ONLY_SECRETS, NULL);
+
+    str_all    = g_variant_print(dict_all, TRUE);
+    str_nonsec = g_variant_print(dict_nonsec, TRUE);
+    str_sec    = g_variant_print(dict_sec, TRUE);
+
+    g_assert_cmpstr(exp_all, ==, str_all);
+    g_assert_cmpstr(exp_nonsec, ==, str_nonsec);
+    g_assert_cmpstr(exp_sec, ==, str_sec);
+}
+
+static void
+test_wireguard_to_dbus(void)
+{
+    gs_unref_object NMSetting            *s_wg              = NULL;
+    nm_auto_unref_wgpeer NMWireGuardPeer *peer1             = NULL;
+    nm_auto_unref_wgpeer NMWireGuardPeer *peer2             = NULL;
+    gs_unref_variant GVariant            *dict_all          = NULL;
+    gs_unref_variant GVariant            *dict_non_secret   = NULL;
+    gs_unref_variant GVariant            *dict_only_secrets = NULL;
+    gs_free char                         *dict_str          = NULL;
+    const char *test_private_key   = "cFoJbK9bSrYrQrjFQGgqsWTO4IUIX0+rsaqNeCw2IWM=";
+    const char *test_public_key1   = "OMhgSum5+NamArI/LTp1mCZQD+CbzZxtOuvDC/RaGWU=";
+    const char *test_public_key2   = "2S7mA0vEMethVGG0qBm4T5EXbcQ2WYHOuP14Seb7jEM=";
+    const char *test_preshared_key = "yFGq76ej4lNI0pLLu36L0DgJMxWs4HmH5qNDNOt8AmM=";
+
+    /* Test case 1: Minimal WireGuard setting without peers or private key */
+    s_wg = nm_setting_wireguard_new();
+    g_object_set(s_wg,
+                 NM_SETTING_WIREGUARD_LISTEN_PORT,
+                 51820U,
+                 NM_SETTING_WIREGUARD_FWMARK,
+                 42U,
+                 NULL);
+
+    check_wg_setting_str(s_wg,
+                         /* clang-format off */
+                         /* all */
+                         "{'fwmark': <uint32 42>, 'listen-port': <uint32 51820>}",
+                         /* non secrets */
+                         "{'fwmark': <uint32 42>, 'listen-port': <uint32 51820>}",
+                         /* secrets */
+                         "@a{sv} {}"
+                         /* clang-format on */
+    );
+    g_clear_object(&s_wg);
+
+    /* Test case 2: WireGuard setting with private key, no peers */
+    s_wg = nm_setting_wireguard_new();
+    g_object_set(s_wg,
+                 NM_SETTING_WIREGUARD_PRIVATE_KEY,
+                 test_private_key,
+                 NM_SETTING_WIREGUARD_PRIVATE_KEY_FLAGS,
+                 NM_SETTING_SECRET_FLAG_NONE,
+                 NM_SETTING_WIREGUARD_LISTEN_PORT,
+                 51820U,
+                 NM_SETTING_WIREGUARD_FWMARK,
+                 42U,
+                 NULL);
+
+    check_wg_setting_str(s_wg,
+                         /* clang-format off */
+                         /* all */
+                         "{"
+                             "'fwmark': <uint32 42>, "
+                             "'listen-port': <uint32 51820>, "
+                             "'private-key': <'cFoJbK9bSrYrQrjFQGgqsWTO4IUIX0+rsaqNeCw2IWM='>"
+                         "}",
+                         /* non secrets */
+                         "{"
+                             "'fwmark': <uint32 42>, "
+                             "'listen-port': <uint32 51820>"
+                         "}",
+                         /* secrets */
+                         "{"
+                             "'private-key': <'cFoJbK9bSrYrQrjFQGgqsWTO4IUIX0+rsaqNeCw2IWM='>"
+                         "}"
+                         /* clang-format on */
+    );
+    g_clear_object(&s_wg);
+
+    /* Test case 3: WireGuard setting with peers (no PSK) */
+    s_wg = nm_setting_wireguard_new();
+    g_object_set(s_wg,
+                 NM_SETTING_WIREGUARD_PRIVATE_KEY,
+                 test_private_key,
+                 NM_SETTING_WIREGUARD_PRIVATE_KEY_FLAGS,
+                 NM_SETTING_SECRET_FLAG_NONE,
+                 NM_SETTING_WIREGUARD_LISTEN_PORT,
+                 51820U,
+                 NULL);
+    peer1 = nm_wireguard_peer_new();
+    nm_wireguard_peer_set_public_key(peer1, test_public_key1, FALSE);
+    nm_wireguard_peer_set_endpoint(peer1, "192.168.1.1:51820", FALSE);
+    nm_wireguard_peer_append_allowed_ip(peer1, "10.0.0.0/8", FALSE);
+    nm_setting_wireguard_append_peer(NM_SETTING_WIREGUARD(s_wg), peer1);
+
+    check_wg_setting_str(s_wg,
+                         /* clang-format off */
+                         /* all */
+                         "{"
+                             "'listen-port': <uint32 51820>, "
+                             "'peers': <[{"
+                                 "'public-key': <'OMhgSum5+NamArI/LTp1mCZQD+CbzZxtOuvDC/RaGWU='>, "
+                                 "'endpoint': <'192.168.1.1:51820'>, "
+                                 "'allowed-ips': <['10.0.0.0/8']>"
+                             "}]>, "
+                             "'private-key': <'cFoJbK9bSrYrQrjFQGgqsWTO4IUIX0+rsaqNeCw2IWM='>"
+                         "}",
+                         /* non secrets */
+                         "{"
+                             "'listen-port': <uint32 51820>, "
+                             "'peers': <[{"
+                                 "'public-key': <'OMhgSum5+NamArI/LTp1mCZQD+CbzZxtOuvDC/RaGWU='>, "
+                                 "'endpoint': <'192.168.1.1:51820'>, "
+                                 "'allowed-ips': <['10.0.0.0/8']>"
+                             "}]>"
+                         "}",
+                         /* secrets */
+                         "{"
+                             "'private-key': <'cFoJbK9bSrYrQrjFQGgqsWTO4IUIX0+rsaqNeCw2IWM='>"
+                         "}"
+                         /* clang-format on */
+    );
+    g_clear_object(&s_wg);
+    nm_clear_pointer(&peer1, nm_wireguard_peer_unref);
+
+    /* Test case 4: WireGuard setting with peers, one has PSK */
+    s_wg = nm_setting_wireguard_new();
+    g_object_set(s_wg,
+                 NM_SETTING_WIREGUARD_PRIVATE_KEY,
+                 test_private_key,
+                 NM_SETTING_WIREGUARD_PRIVATE_KEY_FLAGS,
+                 NM_SETTING_SECRET_FLAG_NONE,
+                 NM_SETTING_WIREGUARD_LISTEN_PORT,
+                 51820U,
+                 NULL);
+
+    /* Peer without PSK */
+    peer1 = nm_wireguard_peer_new();
+    nm_wireguard_peer_set_public_key(peer1, test_public_key1, FALSE);
+    nm_wireguard_peer_set_endpoint(peer1, "192.168.1.1:51820", FALSE);
+    nm_wireguard_peer_append_allowed_ip(peer1, "10.0.0.0/8", FALSE);
+    nm_setting_wireguard_append_peer(NM_SETTING_WIREGUARD(s_wg), peer1);
+
+    /* Peer with PSK */
+    peer2 = nm_wireguard_peer_new();
+    nm_wireguard_peer_set_public_key(peer2, test_public_key2, FALSE);
+    nm_wireguard_peer_set_endpoint(peer2, "192.168.2.1:51820", FALSE);
+    nm_wireguard_peer_append_allowed_ip(peer2, "172.16.0.0/12", FALSE);
+    nm_wireguard_peer_set_preshared_key(peer2, test_preshared_key, FALSE);
+    nm_wireguard_peer_set_preshared_key_flags(peer2, NM_SETTING_SECRET_FLAG_NONE);
+    nm_setting_wireguard_append_peer(NM_SETTING_WIREGUARD(s_wg), peer2);
+
+    check_wg_setting_str(s_wg,
+                         /* clang-format off */
+                         /* all */
+                         "{"
+                             "'listen-port': <uint32 51820>, "
+                             "'peers': <[{"
+                                 "'public-key': <'OMhgSum5+NamArI/LTp1mCZQD+CbzZxtOuvDC/RaGWU='>, "
+                                 "'endpoint': <'192.168.1.1:51820'>, "
+                                 "'allowed-ips': <['10.0.0.0/8']>"
+                             "}, {"
+                                 "'public-key': <'2S7mA0vEMethVGG0qBm4T5EXbcQ2WYHOuP14Seb7jEM='>, "
+                                 "'endpoint': <'192.168.2.1:51820'>, "
+                                 "'preshared-key': <'yFGq76ej4lNI0pLLu36L0DgJMxWs4HmH5qNDNOt8AmM='>, "
+                                 "'preshared-key-flags': <uint32 0>, "
+                                 "'allowed-ips': <['172.16.0.0/12']>"
+                             "}]>, "
+                             "'private-key': <'cFoJbK9bSrYrQrjFQGgqsWTO4IUIX0+rsaqNeCw2IWM='>"
+                         "}",
+                         /* non secrets */
+                         "{"
+                             "'listen-port': <uint32 51820>, "
+                             "'peers': <[{"
+                                 "'public-key': <'OMhgSum5+NamArI/LTp1mCZQD+CbzZxtOuvDC/RaGWU='>, "
+                                 "'endpoint': <'192.168.1.1:51820'>, "
+                                 "'allowed-ips': <['10.0.0.0/8']>"
+                             "}, {"
+                                 "'public-key': <'2S7mA0vEMethVGG0qBm4T5EXbcQ2WYHOuP14Seb7jEM='>, "
+                                 "'endpoint': <'192.168.2.1:51820'>, "
+                                 "'preshared-key-flags': <uint32 0>, "
+                                 "'allowed-ips': <['172.16.0.0/12']>"
+                             "}]>"
+                         "}",
+                         /* secrets */
+                         "{"
+                             "'peers': <[{"
+                                 "'public-key': <'2S7mA0vEMethVGG0qBm4T5EXbcQ2WYHOuP14Seb7jEM='>, "
+                                 "'preshared-key': <'yFGq76ej4lNI0pLLu36L0DgJMxWs4HmH5qNDNOt8AmM='>"
+                             "}]>, "
+                         "'private-key': <'cFoJbK9bSrYrQrjFQGgqsWTO4IUIX0+rsaqNeCw2IWM='>}"
+                         /* clang-format on */
+    );
+    g_clear_object(&s_wg);
+    nm_clear_pointer(&peer1, nm_wireguard_peer_unref);
+    nm_clear_pointer(&peer2, nm_wireguard_peer_unref);
+}
+
+/*****************************************************************************/
+
 NMTST_DEFINE();
 
 int
@@ -5649,5 +5866,7 @@ main(int argc, char **argv)
 
     g_test_add_func("/libnm/test_bond_meta", test_bond_meta);
 
+    g_test_add_func("/libnm/test_wireguard_to_dbus", test_wireguard_to_dbus);
+
     return g_test_run();
 }
diff --git a/src/nm-initrd-generator/nmi-cmdline-reader.c b/src/nm-initrd-generator/nmi-cmdline-reader.c
index ba5380af..fdd7283e 100644
--- a/src/nm-initrd-generator/nmi-cmdline-reader.c
+++ b/src/nm-initrd-generator/nmi-cmdline-reader.c
@@ -1350,6 +1350,67 @@ reader_parse_ethtool(Reader *reader, char *argument)
 }
 
 static void
+reader_parse_dhcp_client_id(Reader *reader, char *argument)
+{
+    NMConnection      *connection;
+    NMSettingIPConfig *s_ip4;
+    const char        *interface;
+    gs_free char      *client_id = NULL;
+    gs_free guint8    *buf       = NULL;
+    gsize              len       = 0;
+
+    interface = get_word(&argument, ':');
+    if (!interface) {
+        _LOGW(LOGD_CORE, "rd.net.dhcp.client-id: missing interface");
+        return;
+    }
+
+    if (!argument || !*argument) {
+        _LOGW(LOGD_CORE, "rd.net.dhcp.client-id: missing client-id");
+        return;
+    }
+
+    if (argument[0] == '@') {
+        /* The client-id is a plain string but we still encode it as
+         * hex string. Otherwise, we could pass the string as-is, but we
+         * would need to handle special keywords like "mac", "perm-mac", etc.
+         */
+        if (argument[1] != '\0') {
+            len    = strlen(argument);
+            buf    = (guint8 *) nm_memdup(argument, len + 1);
+            buf[0] = '\0';
+        }
+    } else {
+        /* Try to parse it as hex string */
+        buf = nm_utils_hexstr2bin_alloc(argument, FALSE, FALSE, "-", 0, &len);
+    }
+
+    if (buf) {
+        client_id = nm_utils_bin2hexstr_full(buf, len, ':', FALSE, NULL);
+    }
+
+    if (!client_id) {
+        _LOGW(LOGD_CORE,
+              "rd.net.dhcp.client-id: invalid client-id \"%s\". Must be hexadecimal bytes "
+              "separated by dashes (for example \"00-01-02-03-04-05-06\"), or '@' followed by a "
+              "string",
+              argument);
+        return;
+    }
+
+    if (len < 2) {
+        _LOGW(LOGD_CORE,
+              "rd.net.dhcp.client-id: invalid client-id \"%s\". Must be at least two bytes",
+              argument);
+        return;
+    }
+
+    connection = reader_get_connection(reader, interface, NULL, TRUE);
+    s_ip4      = nm_connection_get_setting_ip4_config(connection);
+    g_object_set(s_ip4, NM_SETTING_IP4_CONFIG_DHCP_CLIENT_ID, client_id, NULL);
+}
+
+static void
 _normalize_conn(gpointer key, gpointer value, gpointer user_data)
 {
     NMConnection      *connection = value;
@@ -1365,6 +1426,8 @@ _normalize_conn(gpointer key, gpointer value, gpointer user_data)
                          NULL,
                          NM_SETTING_IP_CONFIG_DHCP_TIMEOUT,
                          NULL,
+                         NM_SETTING_IP4_CONFIG_DHCP_CLIENT_ID,
+                         NULL,
                          NM_SETTING_IP4_CONFIG_DHCP_VENDOR_CLASS_IDENTIFIER,
                          NULL,
                          NM_SETTING_IP_CONFIG_DHCP_DSCP,
@@ -1583,6 +1646,8 @@ nmi_cmdline_reader_parse(const char        *etc_connections_dir,
             g_ptr_array_add(znets, g_strdup(argument));
         } else if (nm_streq(tag, "rd.znet_ifname")) {
             reader_parse_znet_ifname(reader, argument);
+        } else if (nm_streq(tag, "rd.net.dhcp.client-id")) {
+            reader_parse_dhcp_client_id(reader, argument);
         } else if (g_ascii_strcasecmp(tag, "BOOTIF") == 0) {
             nm_clear_g_free(&bootif_val);
             bootif_val = g_strdup(argument);
diff --git a/src/nm-initrd-generator/nmi-nbft-reader.c b/src/nm-initrd-generator/nmi-nbft-reader.c
index 5d12086f..dfab0390 100644
--- a/src/nm-initrd-generator/nmi-nbft-reader.c
+++ b/src/nm-initrd-generator/nmi-nbft-reader.c
@@ -94,6 +94,33 @@ find_conn_for_wired_mac(GPtrArray *a, const char *hwaddr)
     return NULL;
 }
 
+static gboolean
+hfi_is_dhcp(struct nbft_info_hfi *hfi, int family)
+{
+    /* There are several flags that may indicate the HFI is set for DHCP
+     * per NVM Express® Boot Specification, Revision 1.3. As the HFI
+     * Transport Flags (HFITFLAGS) are not publicly exposed by the libnvme
+     * API, only the DHCP Override (DHCPO) flag and the IP Origin (IPORIG)
+     * value is available.
+     *
+     * The bit 03 of the HFI Transport Flags (HFITFLAGS) is about an advanced
+     * stateless mechanism - IPv6-SLAAC and IPv6-ND, stating that "the DHCP
+     * Override bit shall be cleared to 0, and the IP Origin field shall
+     * be cleared to 0". This nm-initrd-generator will ignore this flag,
+     * expecting an IP address to be provided by the usual HFI fields
+     * just like in a static adressing case.
+     *
+     * DHCP Override (DHCPO): "The HFI information was populated by
+     * consuming the DHCP on this interface."
+     *
+     * IP Origin (IPORIG): "If set to 3h (IpPrefixOriginDhcp), then the
+     * IP Address was acquired through DHCP, and the IP Address specified
+     * in this HFI should not be reused by the OS."
+     */
+    return hfi->tcp_info.dhcp_override || hfi->tcp_info.ip_origin == 3 /* IpPrefixOriginDhcp */
+           || is_valid_addr(family, hfi->tcp_info.dhcp_server_ipaddr);
+}
+
 static NMConnection *
 create_wired_conn(struct nbft_info_hfi *hfi,
                   const char           *conn_name,
@@ -183,6 +210,7 @@ parse_hfi(GPtrArray *a, struct nbft_info_hfi *hfi, const char *table_name, char
                 return;
             }
             g_ptr_array_add(a, parent_connection);
+            nm_clear_g_free(&conn_name);
         }
 
         conn_name  = format_conn_name(table_name, hfi, TRUE);
@@ -208,7 +236,7 @@ parse_hfi(GPtrArray *a, struct nbft_info_hfi *hfi, const char *table_name, char
                      NM_SETTING_IP_CONFIG_METHOD,
                      NM_SETTING_IP6_CONFIG_METHOD_DISABLED,
                      NULL);
-        if (is_valid_addr(AF_INET, hfi->tcp_info.dhcp_server_ipaddr)) {
+        if (hfi_is_dhcp(hfi, family)) {
             g_object_set(s_ip4,
                          NM_SETTING_IP_CONFIG_METHOD,
                          NM_SETTING_IP4_CONFIG_METHOD_AUTO,
@@ -260,7 +288,7 @@ parse_hfi(GPtrArray *a, struct nbft_info_hfi *hfi, const char *table_name, char
                      NM_SETTING_IP_CONFIG_METHOD,
                      NM_SETTING_IP4_CONFIG_METHOD_DISABLED,
                      NULL);
-        if (is_valid_addr(AF_INET6, hfi->tcp_info.dhcp_server_ipaddr)) {
+        if (hfi_is_dhcp(hfi, family)) {
             g_object_set(s_ip6,
                          NM_SETTING_IP_CONFIG_METHOD,
                          NM_SETTING_IP6_CONFIG_METHOD_AUTO,
diff --git a/src/nm-initrd-generator/tests/meson.build b/src/nm-initrd-generator/tests/meson.build
index 8fa3bcac..897a2148 100644
--- a/src/nm-initrd-generator/tests/meson.build
+++ b/src/nm-initrd-generator/tests/meson.build
@@ -6,6 +6,10 @@ test_units = [
   'test-cmdline-reader',
 ]
 
+if enable_nbft
+  test_units += [ 'test-nbft-reader' ]
+endif
+
 foreach test_unit : test_units
   exe = executable(
     test_unit,
diff --git a/src/nm-initrd-generator/tests/nbft-ipv4-dhcp/firmware/acpi/tables/NBFT-Dell.PowerEdge.R760 b/src/nm-initrd-generator/tests/nbft-ipv4-dhcp/firmware/acpi/tables/NBFT-Dell.PowerEdge.R760
new file mode 100644
index 00000000..2dea9363
--- /dev/null
+++ b/src/nm-initrd-generator/tests/nbft-ipv4-dhcp/firmware/acpi/tables/NBFT-Dell.PowerEdge.R760
Binary files differdiff --git a/src/nm-initrd-generator/tests/nbft-ipv4-dhcp/firmware/acpi/tables/NBFT-dhcp-ipv4 b/src/nm-initrd-generator/tests/nbft-ipv4-dhcp/firmware/acpi/tables/NBFT-dhcp-ipv4
new file mode 100644
index 00000000..1af159de
--- /dev/null
+++ b/src/nm-initrd-generator/tests/nbft-ipv4-dhcp/firmware/acpi/tables/NBFT-dhcp-ipv4
Binary files differdiff --git a/src/nm-initrd-generator/tests/nbft-ipv4-static/firmware/acpi/tables/NBFT-Dell.PowerEdge.R660-fw1.5.5-mpath+discovery b/src/nm-initrd-generator/tests/nbft-ipv4-static/firmware/acpi/tables/NBFT-Dell.PowerEdge.R660-fw1.5.5-mpath+discovery
new file mode 100644
index 00000000..0fbb35cf
--- /dev/null
+++ b/src/nm-initrd-generator/tests/nbft-ipv4-static/firmware/acpi/tables/NBFT-Dell.PowerEdge.R660-fw1.5.5-mpath+discovery
Binary files differdiff --git a/src/nm-initrd-generator/tests/nbft-ipv4-static/firmware/acpi/tables/NBFT-multi b/src/nm-initrd-generator/tests/nbft-ipv4-static/firmware/acpi/tables/NBFT-multi
new file mode 100644
index 00000000..c34875bc
--- /dev/null
+++ b/src/nm-initrd-generator/tests/nbft-ipv4-static/firmware/acpi/tables/NBFT-multi
Binary files differdiff --git a/src/nm-initrd-generator/tests/nbft-ipv4-static/firmware/acpi/tables/NBFT-rhpoc b/src/nm-initrd-generator/tests/nbft-ipv4-static/firmware/acpi/tables/NBFT-rhpoc
new file mode 100644
index 00000000..5d0a6cc0
--- /dev/null
+++ b/src/nm-initrd-generator/tests/nbft-ipv4-static/firmware/acpi/tables/NBFT-rhpoc
Binary files differdiff --git a/src/nm-initrd-generator/tests/nbft-ipv4-static/firmware/acpi/tables/NBFT-static-ipv4 b/src/nm-initrd-generator/tests/nbft-ipv4-static/firmware/acpi/tables/NBFT-static-ipv4
new file mode 100644
index 00000000..bf3f8404
--- /dev/null
+++ b/src/nm-initrd-generator/tests/nbft-ipv4-static/firmware/acpi/tables/NBFT-static-ipv4
Binary files differdiff --git a/src/nm-initrd-generator/tests/nbft-ipv6-auto/firmware/acpi/tables/NBFT-auto-ipv6 b/src/nm-initrd-generator/tests/nbft-ipv6-auto/firmware/acpi/tables/NBFT-auto-ipv6
new file mode 100644
index 00000000..64457d7d
--- /dev/null
+++ b/src/nm-initrd-generator/tests/nbft-ipv6-auto/firmware/acpi/tables/NBFT-auto-ipv6
Binary files differdiff --git a/src/nm-initrd-generator/tests/nbft-ipv6-auto/firmware/acpi/tables/NBFT-dhcp-ipv6 b/src/nm-initrd-generator/tests/nbft-ipv6-auto/firmware/acpi/tables/NBFT-dhcp-ipv6
new file mode 100644
index 00000000..20715eeb
--- /dev/null
+++ b/src/nm-initrd-generator/tests/nbft-ipv6-auto/firmware/acpi/tables/NBFT-dhcp-ipv6
Binary files differdiff --git a/src/nm-initrd-generator/tests/nbft-ipv6-auto/firmware/acpi/tables/NBFT-ipv6-noip+disc b/src/nm-initrd-generator/tests/nbft-ipv6-auto/firmware/acpi/tables/NBFT-ipv6-noip+disc
new file mode 100644
index 00000000..7a0690cb
--- /dev/null
+++ b/src/nm-initrd-generator/tests/nbft-ipv6-auto/firmware/acpi/tables/NBFT-ipv6-noip+disc
Binary files differdiff --git a/src/nm-initrd-generator/tests/nbft-ipv6-static/firmware/acpi/tables/NBFT-static-ipv6 b/src/nm-initrd-generator/tests/nbft-ipv6-static/firmware/acpi/tables/NBFT-static-ipv6
new file mode 100644
index 00000000..07b09cfb
--- /dev/null
+++ b/src/nm-initrd-generator/tests/nbft-ipv6-static/firmware/acpi/tables/NBFT-static-ipv6
Binary files differdiff --git a/src/nm-initrd-generator/tests/nbft-vlan/firmware/acpi/tables/NBFT-qemu-vlans-incomplete b/src/nm-initrd-generator/tests/nbft-vlan/firmware/acpi/tables/NBFT-qemu-vlans-incomplete
new file mode 100644
index 00000000..da385318
--- /dev/null
+++ b/src/nm-initrd-generator/tests/nbft-vlan/firmware/acpi/tables/NBFT-qemu-vlans-incomplete
Binary files differdiff --git a/src/nm-initrd-generator/tests/test-cmdline-reader.c b/src/nm-initrd-generator/tests/test-cmdline-reader.c
index cd7b1069..ac722872 100644
--- a/src/nm-initrd-generator/tests/test-cmdline-reader.c
+++ b/src/nm-initrd-generator/tests/test-cmdline-reader.c
@@ -2786,6 +2786,149 @@ test_plain_equal_char(void)
 
 /*****************************************************************************/
 
+#define _dhcp_client_id_check_invalid(arg)                        \
+    G_STMT_START                                                  \
+    {                                                             \
+        gs_unref_hashtable GHashTable *_connections2 = NULL;      \
+                                                                  \
+        _connections2 = _parse_cons(NM_MAKE_STRV(arg));           \
+        g_test_assert_expected_messages();                        \
+        g_assert_cmpint(g_hash_table_size(_connections2), ==, 0); \
+    }                                                             \
+    G_STMT_END
+
+#define _dhcp_client_id_check_v(strv, exp_ifname, exp_client_id)                                 \
+    G_STMT_START                                                                                 \
+    {                                                                                            \
+        gs_unref_object NMConnection *_connection = NULL;                                        \
+        NMSettingIPConfig            *_s_ip4;                                                    \
+                                                                                                 \
+        _connection = _parse_con(strv, exp_ifname);                                              \
+                                                                                                 \
+        g_test_assert_expected_messages();                                                       \
+                                                                                                 \
+        g_assert(nm_connection_get_setting_connection(_connection));                             \
+        g_assert(nm_connection_is_type(_connection, NM_SETTING_WIRED_SETTING_NAME));             \
+        g_assert(nm_connection_get_setting_ip4_config(_connection));                             \
+        g_assert(nm_connection_get_setting_ip6_config(_connection));                             \
+        _s_ip4 = nm_connection_get_setting_ip4_config(_connection);                              \
+        g_assert(NM_IS_SETTING_IP_CONFIG(_s_ip4));                                               \
+                                                                                                 \
+        g_assert_cmpstr(nm_setting_ip4_config_get_dhcp_client_id(NM_SETTING_IP4_CONFIG(_s_ip4)), \
+                        ==,                                                                      \
+                        (exp_client_id));                                                        \
+    }                                                                                            \
+    G_STMT_END
+
+#define _dhcp_client_id_check(arg, exp_ifname, exp_client_id) \
+    _dhcp_client_id_check_v(NM_MAKE_STRV("" arg ""), (exp_ifname), (exp_client_id))
+
+#define DHCP_CLIENT_ID_INVALID_MSG(_id)                                                \
+    "cmdline-reader: "                                                                 \
+    "rd.net.dhcp.client-id: invalid client-id \"" _id "\". Must be hexadecimal bytes " \
+    "separated by dashes (for example \"00-01-02-03-04-05-06\"), or '@' followed by a string"
+
+static void
+test_rd_dhcp_client_id(void)
+{
+    NMTST_EXPECT_NM_WARN("cmdline-reader: rd.net.dhcp.client-id: missing interface");
+    _dhcp_client_id_check_invalid("rd.net.dhcp.client-id=");
+
+    NMTST_EXPECT_NM_WARN("cmdline-reader: rd.net.dhcp.client-id: missing interface");
+    _dhcp_client_id_check_invalid("rd.net.dhcp.client-id=:");
+
+    NMTST_EXPECT_NM_WARN("cmdline-reader: rd.net.dhcp.client-id: missing client-id");
+    _dhcp_client_id_check_invalid("rd.net.dhcp.client-id=eth0:");
+
+    NMTST_EXPECT_NM_WARN(DHCP_CLIENT_ID_INVALID_MSG("invalid"));
+    _dhcp_client_id_check_invalid("rd.net.dhcp.client-id=eth0:invalid");
+
+    NMTST_EXPECT_NM_WARN(DHCP_CLIENT_ID_INVALID_MSG("01:AA:BB:CC:DD:EE:FF"));
+    _dhcp_client_id_check_invalid("rd.net.dhcp.client-id=eth0:01:AA:BB:CC:DD:EE:FF");
+
+    NMTST_EXPECT_NM_WARN(DHCP_CLIENT_ID_INVALID_MSG("@"));
+    _dhcp_client_id_check_invalid("rd.net.dhcp.client-id=eth0:@");
+
+    NMTST_EXPECT_NM_WARN("cmdline-reader: rd.net.dhcp.client-id: invalid client-id \"01\". Must be "
+                         "at least two bytes");
+    _dhcp_client_id_check_invalid("rd.net.dhcp.client-id=eth0:01");
+
+    /* Client-id with hex string */
+    _dhcp_client_id_check("rd.net.dhcp.client-id=eth0:01-aa-BB-cc-dd-EE-ff",
+                          "eth0",
+                          "01:aa:bb:cc:dd:ee:ff");
+
+    /* Client-id with plain string */
+    _dhcp_client_id_check("rd.net.dhcp.client-id=eth0:@test.com",
+                          "eth0",
+                          "00:74:65:73:74:2e:63:6f:6d");
+
+    /* Minimal client-id, hex */
+    _dhcp_client_id_check("rd.net.dhcp.client-id=eth1:01-02", "eth1", "01:02");
+
+    /* Minimal client-id, string */
+    _dhcp_client_id_check("rd.net.dhcp.client-id=eth1:@1", "eth1", "00:31");
+
+    /* Long client-id */
+    _dhcp_client_id_check(
+        "rd.net.dhcp.client-id=enp1s0:"
+        "01-02-03-04-05-06-07-08-09-10-11-12-13-14-15-16-17-18-19-20-21-22-23-24-"
+        "25-26-27-28-29-30-31-32-33-34-35-36-37-38-39-40-41-42-43-44-45-46-47-48-"
+        "49-50-51-52-53-54-55-56-57-58-59-60-61-62-63-64-65-66-67-68-69-70-71-72",
+        "enp1s0",
+        "01:02:03:04:05:06:07:08:09:10:11:12:13:14:15:16:17:18:19:20:21:22:23:24:"
+        "25:26:27:28:29:30:31:32:33:34:35:36:37:38:39:40:41:42:43:44:45:46:47:48:"
+        "49:50:51:52:53:54:55:56:57:58:59:60:61:62:63:64:65:66:67:68:69:70:71:72");
+
+    /* Test ordering: client-id before ip= */
+    _dhcp_client_id_check_v(
+        NM_MAKE_STRV("rd.net.dhcp.client-id=eth0:aa-bb-cc-dd-ee-ff", "ip=eth0:dhcp"),
+        "eth0",
+        "aa:bb:cc:dd:ee:ff");
+
+    /* Test ordering: client-id after ip= */
+    _dhcp_client_id_check_v(
+        NM_MAKE_STRV("ip=eth2:dhcp", "rd.net.dhcp.client-id=eth2:ba-da-cc-dd-ee-ff"),
+        "eth2",
+        "ba:da:cc:dd:ee:ff");
+
+    /* Duplicate option: last wins */
+    _dhcp_client_id_check_v(NM_MAKE_STRV("ip=eth3:dhcp",
+                                         "rd.net.dhcp.client-id=eth3:01-02",
+                                         "rd.net.dhcp.client-id=eth3:01-03"),
+                            "eth3",
+                            "01:03");
+
+    /* Multiple connections */
+    {
+        gs_unref_hashtable GHashTable *connections = NULL;
+        NMConnection                  *connection;
+        NMSettingIP4Config            *s_ip4;
+
+        connections = _parse_cons(NM_MAKE_STRV("ip=eth0:dhcp",
+                                               "ip=eth1:dhcp",
+                                               "rd.net.dhcp.client-id=eth1:01-01-01",
+                                               "rd.net.dhcp.client-id=eth0:00-00-00"));
+
+        g_assert_nonnull(connections);
+        g_assert_cmpint(g_hash_table_size(connections), ==, 2);
+
+        connection = g_hash_table_lookup(connections, "eth0");
+        g_assert_nonnull(connection);
+        s_ip4 = (NMSettingIP4Config *) nm_connection_get_setting_ip4_config(connection);
+        g_assert_nonnull(s_ip4);
+        g_assert_cmpstr(nm_setting_ip4_config_get_dhcp_client_id(s_ip4), ==, "00:00:00");
+
+        connection = g_hash_table_lookup(connections, "eth1");
+        g_assert_nonnull(connection);
+        s_ip4 = (NMSettingIP4Config *) nm_connection_get_setting_ip4_config(connection);
+        g_assert_nonnull(s_ip4);
+        g_assert_cmpstr(nm_setting_ip4_config_get_dhcp_client_id(s_ip4), ==, "01:01:01");
+    }
+}
+
+/*****************************************************************************/
+
 NMTST_DEFINE();
 
 int
@@ -2848,6 +2991,7 @@ main(int argc, char **argv)
     g_test_add_func("/initrd/cmdline/rd_ethtool", test_rd_ethtool);
     g_test_add_func("/initrd/cmdline/plain_equal_char", test_plain_equal_char);
     g_test_add_func("/initrd/cmdline/global_dns", test_global_dns);
+    g_test_add_func("/initrd/cmdline/rd_dhcp_client_id", test_rd_dhcp_client_id);
 
     return g_test_run();
 }
diff --git a/src/nm-initrd-generator/tests/test-nbft-reader.c b/src/nm-initrd-generator/tests/test-nbft-reader.c
new file mode 100644
index 00000000..f93d3d76
--- /dev/null
+++ b/src/nm-initrd-generator/tests/test-nbft-reader.c
@@ -0,0 +1,397 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2025 Red Hat, Inc.
+ */
+
+#include "src/core/nm-default-daemon.h"
+
+#include "libnm-core-intern/nm-core-internal.h"
+#include "libnm-glib-aux/nm-test-utils.h"
+
+#include "nm-initrd-generator/nm-initrd-generator.h"
+
+#define TEST_INITRD_DIR NM_BUILD_SRCDIR "/src/nm-initrd-generator/tests"
+
+/*****************************************************************************/
+
+static NMConnection *
+find_connection_for_mac(NMConnection **nbft_connections, const char *expected_mac, guint32 vlan_id)
+{
+    NMConnection  **c;
+    NMSettingWired *s_wired;
+    NMSettingVlan  *s_vlan;
+    const char     *mac_address;
+
+    for (c = nbft_connections; c && *c; c++) {
+        s_wired = nm_connection_get_setting_wired(*c);
+        g_assert(s_wired);
+        mac_address = nm_setting_wired_get_mac_address(s_wired);
+        g_assert(mac_address);
+        if (!nm_utils_hwaddr_matches(mac_address, -1, expected_mac, -1))
+            continue;
+        s_vlan = nm_connection_get_setting_vlan(*c);
+        if (vlan_id > 0) {
+            if (!s_vlan)
+                continue;
+            if (nm_setting_vlan_get_id(s_vlan) != vlan_id)
+                continue;
+        } else if (s_vlan)
+            continue;
+        return *c;
+    }
+
+    return NULL;
+}
+
+static void
+verify_connection(NMConnection *c, const char *expected_mac, guint32 expected_vlan_id)
+{
+    NMSettingConnection *s_con;
+    NMSettingWired      *s_wired;
+    NMSettingVlan       *s_vlan;
+    const char          *mac_address;
+
+    nmtst_assert_connection_verifies_without_normalization(c);
+
+    s_con = nm_connection_get_setting_connection(c);
+    g_assert(s_con);
+
+    g_assert(g_str_has_prefix(nm_setting_connection_get_id(s_con), "NBFT"));
+    g_assert_cmpstr(nm_setting_connection_get_interface_name(s_con), ==, NULL);
+    g_assert(nm_setting_connection_get_autoconnect_priority(s_con)
+             == NMI_AUTOCONNECT_PRIORITY_FIRMWARE);
+
+    s_wired = nm_connection_get_setting_wired(c);
+    g_assert(s_wired);
+    mac_address = nm_setting_wired_get_mac_address(s_wired);
+    g_assert(mac_address);
+    g_assert(nm_utils_hwaddr_matches(mac_address, -1, expected_mac, -1));
+
+    if (expected_vlan_id > 0) {
+        g_assert_cmpstr(nm_setting_connection_get_connection_type(s_con),
+                        ==,
+                        NM_SETTING_VLAN_SETTING_NAME);
+        s_vlan = nm_connection_get_setting_vlan(c);
+        g_assert(s_vlan);
+        g_assert_cmpint(nm_setting_vlan_get_id(s_vlan), ==, expected_vlan_id);
+        g_assert_cmpstr(nm_setting_vlan_get_parent(s_vlan), ==, NULL);
+    } else {
+        g_assert_cmpstr(nm_setting_connection_get_connection_type(s_con),
+                        ==,
+                        NM_SETTING_WIRED_SETTING_NAME);
+    }
+}
+
+static void
+verify_ipv4(NMConnection *c, const char *addr, int mask, const char *gateway)
+{
+    NMSettingIPConfig *s_ip4;
+    NMIPAddress       *ip4_addr;
+
+    s_ip4 = nm_connection_get_setting_ip4_config(c);
+    g_assert(s_ip4);
+    g_assert_cmpstr(nm_setting_ip_config_get_method(s_ip4),
+                    ==,
+                    NM_SETTING_IP4_CONFIG_METHOD_MANUAL);
+
+    g_assert_cmpint(nm_setting_ip_config_get_num_dns(s_ip4), ==, 0);
+
+    g_assert_cmpint(nm_setting_ip_config_get_num_addresses(s_ip4), ==, 1);
+    ip4_addr = nm_setting_ip_config_get_address(s_ip4, 0);
+    g_assert(ip4_addr);
+    g_assert_cmpstr(nm_ip_address_get_address(ip4_addr), ==, addr);
+    g_assert_cmpint(nm_ip_address_get_prefix(ip4_addr), ==, mask);
+
+    g_assert_cmpstr(nm_setting_ip_config_get_gateway(s_ip4), ==, gateway);
+}
+
+static void
+verify_ipv4_dhcp(NMConnection *c)
+{
+    NMSettingIPConfig *s_ip4;
+
+    s_ip4 = nm_connection_get_setting_ip4_config(c);
+    g_assert(s_ip4);
+    g_assert_cmpstr(nm_setting_ip_config_get_method(s_ip4), ==, NM_SETTING_IP4_CONFIG_METHOD_AUTO);
+}
+
+static void
+verify_ipv4_disabled(NMConnection *c)
+{
+    NMSettingIPConfig *s_ip4;
+
+    s_ip4 = nm_connection_get_setting_ip4_config(c);
+    g_assert(s_ip4);
+    g_assert_cmpstr(nm_setting_ip_config_get_method(s_ip4),
+                    ==,
+                    NM_SETTING_IP4_CONFIG_METHOD_DISABLED);
+}
+
+static void
+verify_ipv6(NMConnection *c, const char *addr, int prefix, const char *gateway)
+{
+    NMSettingIPConfig *s_ip6;
+    NMIPAddress       *ip6_addr;
+
+    s_ip6 = nm_connection_get_setting_ip6_config(c);
+    g_assert(s_ip6);
+    g_assert_cmpstr(nm_setting_ip_config_get_method(s_ip6),
+                    ==,
+                    NM_SETTING_IP6_CONFIG_METHOD_MANUAL);
+
+    g_assert_cmpint(nm_setting_ip_config_get_num_dns(s_ip6), ==, 0);
+
+    g_assert_cmpint(nm_setting_ip_config_get_num_addresses(s_ip6), ==, 1);
+    ip6_addr = nm_setting_ip_config_get_address(s_ip6, 0);
+    g_assert(ip6_addr);
+    g_assert_cmpstr(nm_ip_address_get_address(ip6_addr), ==, addr);
+    g_assert_cmpint(nm_ip_address_get_prefix(ip6_addr), ==, prefix);
+
+    g_assert_cmpstr(nm_setting_ip_config_get_gateway(s_ip6), ==, gateway);
+}
+
+static void
+verify_ipv6_auto(NMConnection *c)
+{
+    NMSettingIPConfig *s_ip6;
+
+    s_ip6 = nm_connection_get_setting_ip6_config(c);
+    g_assert(s_ip6);
+    g_assert_cmpstr(nm_setting_ip_config_get_method(s_ip6), ==, NM_SETTING_IP6_CONFIG_METHOD_AUTO);
+}
+
+static void
+verify_ipv6_disabled(NMConnection *c)
+{
+    NMSettingIPConfig *s_ip6;
+
+    s_ip6 = nm_connection_get_setting_ip6_config(c);
+    g_assert(s_ip6);
+    g_assert_cmpstr(nm_setting_ip_config_get_method(s_ip6),
+                    ==,
+                    NM_SETTING_IP6_CONFIG_METHOD_DISABLED);
+}
+
+static int
+count_nm_conn(NMConnection **connections)
+{
+    int cnt;
+
+    for (cnt = 0; connections && *connections; connections++, cnt++)
+        ;
+
+    return cnt;
+}
+
+static void
+free_connections(NMConnection **connections)
+{
+    NMConnection **c;
+
+    for (c = connections; c && *c; c++)
+        g_object_unref(*c);
+    g_free(connections);
+}
+
+static void
+test_read_nbft_ipv4_static(void)
+{
+    NMConnection **nbft_connections;
+    NMConnection  *connection;
+    const char    *expected_mac_address;
+    gs_free char  *hostname = NULL;
+
+    nbft_connections = nmi_nbft_reader_parse(TEST_INITRD_DIR "/nbft-ipv4-static", &hostname);
+    g_assert_nonnull(hostname);
+    g_assert_cmpint(count_nm_conn(nbft_connections), ==, 6);
+
+    /* NBFT-multi HFI 1 */
+    expected_mac_address = "52:54:00:72:c5:ae";
+    connection           = find_connection_for_mac(nbft_connections, expected_mac_address, 0);
+    verify_connection(connection, expected_mac_address, 0);
+    verify_ipv4(connection, "192.168.122.158", 24, "192.168.122.1");
+    verify_ipv6_disabled(connection);
+
+    /* NBFT-multi HFI 2 */
+    expected_mac_address = "52:54:00:72:c5:af";
+    connection           = find_connection_for_mac(nbft_connections, expected_mac_address, 0);
+    verify_connection(connection, expected_mac_address, 0);
+    verify_ipv4_dhcp(connection);
+    verify_ipv6_disabled(connection);
+
+    /* NBFT-Dell.PowerEdge.R660-fw1.5.5-mpath+discovery HFI 1 */
+    expected_mac_address = "00:62:0b:cb:eb:70";
+    connection           = find_connection_for_mac(nbft_connections, expected_mac_address, 0);
+    verify_connection(connection, expected_mac_address, 0);
+    verify_ipv4(connection, "172.18.240.1", 24, NULL);
+    verify_ipv6_disabled(connection);
+
+    /* NBFT-Dell.PowerEdge.R660-fw1.5.5-mpath+discovery HFI 2 */
+    expected_mac_address = "00:62:0b:cb:eb:71";
+    connection           = find_connection_for_mac(nbft_connections, expected_mac_address, 0);
+    verify_connection(connection, expected_mac_address, 0);
+    verify_ipv4(connection, "172.18.230.2", 24, NULL);
+    verify_ipv6_disabled(connection);
+
+    /* NBFT-rhpoc */
+    expected_mac_address = "ea:eb:d3:58:89:58";
+    connection           = find_connection_for_mac(nbft_connections, expected_mac_address, 0);
+    verify_connection(connection, expected_mac_address, 0);
+    verify_ipv4(connection, "192.168.101.30", 24, NULL);
+    verify_ipv6_disabled(connection);
+
+    /* NBFT-static-ipv4 */
+    expected_mac_address = "52:54:00:b8:19:b9";
+    connection           = find_connection_for_mac(nbft_connections, expected_mac_address, 0);
+    verify_connection(connection, expected_mac_address, 0);
+    verify_ipv4(connection, "192.168.49.50", 24, NULL);
+    verify_ipv6_disabled(connection);
+
+    free_connections(nbft_connections);
+}
+
+static void
+test_read_nbft_ipv4_dhcp(void)
+{
+    NMConnection **nbft_connections;
+    NMConnection  *connection;
+    const char    *expected_mac_address;
+    gs_free char  *hostname = NULL;
+
+    nbft_connections = nmi_nbft_reader_parse(TEST_INITRD_DIR "/nbft-ipv4-dhcp", &hostname);
+    g_assert_nonnull(hostname);
+    g_assert_cmpint(count_nm_conn(nbft_connections), ==, 2);
+
+    /* NBFT-dhcp-ipv4 */
+    expected_mac_address = "52:54:00:b8:19:b9";
+    connection           = find_connection_for_mac(nbft_connections, expected_mac_address, 0);
+    verify_connection(connection, expected_mac_address, 0);
+    verify_ipv4_dhcp(connection);
+    verify_ipv6_disabled(connection);
+
+    /* NBFT-Dell.PowerEdge.R760 */
+    expected_mac_address = "b0:26:28:e8:7c:0e";
+    connection           = find_connection_for_mac(nbft_connections, expected_mac_address, 0);
+    verify_connection(connection, expected_mac_address, 0);
+    verify_ipv4_dhcp(connection);
+    verify_ipv6_disabled(connection);
+
+    free_connections(nbft_connections);
+}
+
+static void
+test_read_nbft_ipv6_static(void)
+{
+    NMConnection **nbft_connections;
+    NMConnection  *connection;
+    const char    *expected_mac_address;
+    gs_free char  *hostname = NULL;
+
+    nbft_connections = nmi_nbft_reader_parse(TEST_INITRD_DIR "/nbft-ipv6-static", &hostname);
+    g_assert_nonnull(hostname);
+    g_assert_cmpint(count_nm_conn(nbft_connections), ==, 1);
+
+    /* NBFT-static-ipv6 */
+    expected_mac_address = "52:54:00:9e:20:1a";
+    connection           = find_connection_for_mac(nbft_connections, expected_mac_address, 0);
+    verify_connection(connection, expected_mac_address, 0);
+    verify_ipv6(connection, "fd09:9a46:b5c1:1fe::10", 64, NULL);
+    verify_ipv4_disabled(connection);
+
+    free_connections(nbft_connections);
+}
+
+static void
+test_read_nbft_ipv6_auto(void)
+{
+    NMConnection **nbft_connections;
+    NMConnection  *connection;
+    const char    *expected_mac_address;
+    gs_free char  *hostname = NULL;
+
+    nbft_connections = nmi_nbft_reader_parse(TEST_INITRD_DIR "/nbft-ipv6-auto", &hostname);
+    g_assert_nonnull(hostname);
+    g_assert_cmpint(count_nm_conn(nbft_connections), ==, 3);
+
+    /* NBFT-auto-ipv6 */
+    expected_mac_address = "52:54:00:9e:20:1a";
+    connection           = find_connection_for_mac(nbft_connections, expected_mac_address, 0);
+    verify_connection(connection, expected_mac_address, 0);
+    verify_ipv6(connection, "fd09:9a46:b5c1:1ff:5054:ff:fe9e:201a", 64, NULL);
+    verify_ipv4_disabled(connection);
+
+    /* NBFT-dhcp-ipv6 */
+    expected_mac_address = "52:54:00:b8:19:b9";
+    connection           = find_connection_for_mac(nbft_connections, expected_mac_address, 0);
+    verify_connection(connection, expected_mac_address, 0);
+    verify_ipv6_auto(connection);
+    verify_ipv4_disabled(connection);
+
+    /* NBFT-ipv6-noip+disc */
+    expected_mac_address = "40:a6:b7:c0:8a:c9";
+    connection           = find_connection_for_mac(nbft_connections, expected_mac_address, 0);
+    verify_connection(connection, expected_mac_address, 0);
+    verify_ipv6_auto(connection);
+    verify_ipv4_disabled(connection);
+
+    free_connections(nbft_connections);
+}
+
+static void
+test_read_nbft_vlan(void)
+{
+    NMConnection **nbft_connections;
+    NMConnection  *connection;
+    const char    *expected_mac_address;
+    gs_free char  *hostname = NULL;
+
+    nbft_connections = nmi_nbft_reader_parse(TEST_INITRD_DIR "/nbft-vlan", &hostname);
+    g_assert_cmpstr(hostname, ==, NULL);
+    g_assert_cmpint(count_nm_conn(nbft_connections), ==, 4);
+
+    /* NBFT-qemu-vlans-incomplete HFI 1 */
+    expected_mac_address = "52:54:00:72:c5:ae";
+    connection           = find_connection_for_mac(nbft_connections, expected_mac_address, 0);
+    verify_connection(connection, expected_mac_address, 0);
+    verify_ipv4(connection, "192.168.122.158", 24, "192.168.122.1");
+    verify_ipv6_disabled(connection);
+
+    /* NBFT-qemu-vlans-incomplete HFI 2 */
+    expected_mac_address = "52:54:00:72:c5:af";
+    connection           = find_connection_for_mac(nbft_connections, expected_mac_address, 0);
+    verify_connection(connection, expected_mac_address, 0);
+    verify_ipv4_disabled(connection);
+    verify_ipv6_disabled(connection);
+
+    /* NBFT-qemu-vlans-incomplete HFI 2 VLAN 11 */
+    expected_mac_address = "52:54:00:72:c5:af";
+    connection           = find_connection_for_mac(nbft_connections, expected_mac_address, 11);
+    verify_connection(connection, expected_mac_address, 11);
+    verify_ipv4(connection, "192.168.124.58", 24, NULL);
+    verify_ipv6_disabled(connection);
+
+    /* NBFT-qemu-vlans-incomplete HFI 2 VLAN 12 */
+    expected_mac_address = "52:54:00:72:c5:af";
+    connection           = find_connection_for_mac(nbft_connections, expected_mac_address, 12);
+    verify_connection(connection, expected_mac_address, 12);
+    verify_ipv4(connection, "192.168.125.58", 24, NULL);
+    verify_ipv6_disabled(connection);
+
+    free_connections(nbft_connections);
+}
+
+NMTST_DEFINE();
+
+int
+main(int argc, char **argv)
+{
+    nmtst_init_assert_logging(&argc, &argv, "INFO", "DEFAULT");
+
+    g_test_add_func("/initrd/nbft/ipv4-static", test_read_nbft_ipv4_static);
+    g_test_add_func("/initrd/nbft/ipv4-dhcp", test_read_nbft_ipv4_dhcp);
+    g_test_add_func("/initrd/nbft/ipv6-static", test_read_nbft_ipv6_static);
+    g_test_add_func("/initrd/nbft/ipv6-auto", test_read_nbft_ipv6_auto);
+    g_test_add_func("/initrd/nbft/vlan", test_read_nbft_vlan);
+
+    return g_test_run();
+}
diff --git a/src/tests/client/test-client.py b/src/tests/client/test-client.py
index cc76d37b..80080056 100755
--- a/src/tests/client/test-client.py
+++ b/src/tests/client/test-client.py
@@ -2247,9 +2247,9 @@ class TestNmcli(unittest.TestCase):
             extra_env=no_dbus_env,
             replace_stderr=[
                 Util.ReplaceTextRegex(
-                    # depending on glib version, it prints `%s', '%s', or “%s”.
-                    # depending on libc version, it converts unicode to ? or *.
-                    r"Key/Value pair 0, [`*?']invalid[*?'], in address element [`*?']very:invalid[*?'] does not contain an equal sign",
+                    # Depending on glib version, it prints `%s', '%s', or “%s”.
+                    # Some libc versions convert the multi-byte UTF-8 sequence to ? or *.
+                    r"Key/Value pair 0, .*invalid.*, in address element .*very:invalid.* does not contain an equal sign",
                     "Key/Value pair 0, 'invalid', in address element 'very:invalid' does not contain an equal sign",
                 )
             ],