Merge remote-tracking branch 'origin/upstream/main' into HEAD

Update OpenThread commit to May 26 version, commit hash
323ffd894ba9dfa6362ed0fa5d8eb9a4f9167d01

Change-Id: I98157abb6ed71cffe378550ba38c95b6d883b746
diff --git a/.code-spell-ignore b/.code-spell-ignore
new file mode 100644
index 0000000..fc981f0
--- /dev/null
+++ b/.code-spell-ignore
@@ -0,0 +1,18 @@
+aanother
+acount
+addrss
+aline
+anumber
+ans
+aother
+aparent
+apending
+asender
+asent
+ect
+nd
+ot
+shashes
+ue
+unknwn
+unsecure
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 9fee425..3b29bde 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -49,7 +49,7 @@
     runs-on: ubuntu-22.04
     steps:
     - name: Harden Runner
-      uses: step-security/harden-runner@1f99358870fe1c846a3ccba386cc2b2246836776 # v2.2.1
+      uses: step-security/harden-runner@6b3083af2869dc3314a0257a42f4af696cc79ba3 # v2.3.1
       with:
         egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs
 
@@ -70,7 +70,7 @@
     runs-on: ubuntu-latest
     steps:
     - name: Harden Runner
-      uses: step-security/harden-runner@1f99358870fe1c846a3ccba386cc2b2246836776 # v2.2.1
+      uses: step-security/harden-runner@6b3083af2869dc3314a0257a42f4af696cc79ba3 # v2.3.1
       with:
         egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs
 
@@ -80,11 +80,30 @@
         use-verbose-mode: 'yes'
         max-depth: 3
 
+  spell-check:
+    runs-on: ubuntu-22.04
+    steps:
+    - name: Harden Runner
+      uses: step-security/harden-runner@6b3083af2869dc3314a0257a42f4af696cc79ba3 # v2.3.1
+      with:
+        egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs
+
+    - uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
+      with:
+        submodules: true
+    - name: Bootstrap
+      run: |
+          python -m pip install --upgrade pip
+          pip install codespell
+    - name: Check
+      run: |
+        script/code-spell check
+
   cmake-version:
     runs-on: ubuntu-20.04
     steps:
     - name: Harden Runner
-      uses: step-security/harden-runner@1f99358870fe1c846a3ccba386cc2b2246836776 # v2.2.1
+      uses: step-security/harden-runner@6b3083af2869dc3314a0257a42f4af696cc79ba3 # v2.3.1
       with:
         egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs
 
@@ -124,7 +143,7 @@
       CXX: ${{ matrix.compiler_cpp }}
     steps:
     - name: Harden Runner
-      uses: step-security/harden-runner@1f99358870fe1c846a3ccba386cc2b2246836776 # v2.2.1
+      uses: step-security/harden-runner@6b3083af2869dc3314a0257a42f4af696cc79ba3 # v2.3.1
       with:
         egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs
 
@@ -143,7 +162,7 @@
     runs-on: ubuntu-22.04
     steps:
     - name: Harden Runner
-      uses: step-security/harden-runner@1f99358870fe1c846a3ccba386cc2b2246836776 # v2.2.1
+      uses: step-security/harden-runner@6b3083af2869dc3314a0257a42f4af696cc79ba3 # v2.3.1
       with:
         egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs
 
@@ -162,7 +181,7 @@
     runs-on: ubuntu-20.04
     steps:
     - name: Harden Runner
-      uses: step-security/harden-runner@1f99358870fe1c846a3ccba386cc2b2246836776 # v2.2.1
+      uses: step-security/harden-runner@6b3083af2869dc3314a0257a42f4af696cc79ba3 # v2.3.1
       with:
         egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs
 
@@ -204,9 +223,18 @@
           - gcc_ver: 9
             gcc_download_url: https://developer.arm.com/-/media/Files/downloads/gnu-rm/9-2019q4/RC2.1/gcc-arm-none-eabi-9-2019-q4-major-x86_64-linux.tar.bz2
             gcc_extract_dir: gcc-arm-none-eabi-9-2019-q4-major
+          - gcc_ver: 10
+            gcc_download_url: https://developer.arm.com/-/media/Files/downloads/gnu-rm/10.3-2021.10/gcc-arm-none-eabi-10.3-2021.10-x86_64-linux.tar.bz2
+            gcc_extract_dir: gcc-arm-none-eabi-10.3-2021.10
+          - gcc_ver: 11
+            gcc_download_url: https://developer.arm.com/-/media/Files/downloads/gnu/11.3.rel1/binrel/arm-gnu-toolchain-11.3.rel1-x86_64-arm-none-eabi.tar.xz
+            gcc_extract_dir: arm-gnu-toolchain-11.3.rel1-x86_64-arm-none-eabi
+          - gcc_ver: 12
+            gcc_download_url: https://developer.arm.com/-/media/Files/downloads/gnu/12.2.rel1/binrel/arm-gnu-toolchain-12.2.rel1-x86_64-arm-none-eabi.tar.xz
+            gcc_extract_dir: arm-gnu-toolchain-12.2.rel1-x86_64-arm-none-eabi
     steps:
     - name: Harden Runner
-      uses: step-security/harden-runner@1f99358870fe1c846a3ccba386cc2b2246836776 # v2.2.1
+      uses: step-security/harden-runner@6b3083af2869dc3314a0257a42f4af696cc79ba3 # v2.3.1
       with:
         egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs
 
@@ -218,8 +246,8 @@
         cd /tmp
         sudo rm /etc/apt/sources.list.d/* && sudo apt-get update
         sudo apt-get --no-install-recommends install -y build-essential lib32z1 ninja-build gcc-arm-linux-gnueabihf g++-arm-linux-gnueabihf
-        wget --tries 4 --no-check-certificate --quiet ${{ matrix.gcc_download_url }} -O gcc-arm.tar.bz2
-        tar xjf gcc-arm.tar.bz2
+        wget --tries 4 --no-check-certificate --quiet ${{ matrix.gcc_download_url }} -O gcc-arm
+        tar xf gcc-arm
         sudo apt-get remove cmake
         sudo apt-get purge --auto-remove cmake
         wget http://www.cmake.org/files/v3.10/cmake-3.10.3.tar.gz
@@ -247,7 +275,7 @@
       CXX: g++-${{ matrix.gcc_ver }}
     steps:
     - name: Harden Runner
-      uses: step-security/harden-runner@1f99358870fe1c846a3ccba386cc2b2246836776 # v2.2.1
+      uses: step-security/harden-runner@6b3083af2869dc3314a0257a42f4af696cc79ba3 # v2.3.1
       with:
         egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs
 
@@ -280,7 +308,7 @@
       CXX: clang++-${{ matrix.clang_ver }}
     steps:
       - name: Harden Runner
-        uses: step-security/harden-runner@1f99358870fe1c846a3ccba386cc2b2246836776 # v2.2.1
+        uses: step-security/harden-runner@6b3083af2869dc3314a0257a42f4af696cc79ba3 # v2.3.1
         with:
           egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs
 
@@ -318,7 +346,7 @@
       LDFLAGS: -m32
     steps:
       - name: Harden Runner
-        uses: step-security/harden-runner@1f99358870fe1c846a3ccba386cc2b2246836776 # v2.2.1
+        uses: step-security/harden-runner@6b3083af2869dc3314a0257a42f4af696cc79ba3 # v2.3.1
         with:
           egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs
 
@@ -346,7 +374,7 @@
     runs-on: ubuntu-20.04
     steps:
     - name: Harden Runner
-      uses: step-security/harden-runner@1f99358870fe1c846a3ccba386cc2b2246836776 # v2.2.1
+      uses: step-security/harden-runner@6b3083af2869dc3314a0257a42f4af696cc79ba3 # v2.3.1
       with:
         egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs
 
@@ -382,7 +410,7 @@
       CXX: ${{ matrix.CXX }}
     steps:
     - name: Harden Runner
-      uses: step-security/harden-runner@1f99358870fe1c846a3ccba386cc2b2246836776 # v2.2.1
+      uses: step-security/harden-runner@6b3083af2869dc3314a0257a42f4af696cc79ba3 # v2.3.1
       with:
         egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs
 
@@ -400,3 +428,33 @@
         export PATH=$(brew --prefix m4)/bin:$PATH
         script/check-posix-build
         script/check-simulation-build
+
+  android-ndk:
+    name: android-ndk
+    runs-on: ubuntu-22.04
+    container:
+      image: openthread/environment
+    steps:
+    - name: Harden Runner
+      uses: step-security/harden-runner@6b3083af2869dc3314a0257a42f4af696cc79ba3 # v2.3.1
+      with:
+        egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs
+
+    - uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
+      with:
+        submodules: true
+    - name: Install unzip
+      run: apt update && apt install -y unzip
+    - name: Setup NDK
+      id: setup-ndk
+      uses: nttld/setup-ndk@v1
+      with:
+        ndk-version: r25c
+        local-cache: true
+
+    - name: Build
+      env:
+        NDK: ${{ steps.setup-ndk.outputs.ndk-path }}
+      run: |
+        rm -rf build/ && OT_CMAKE_NINJA_TARGET="ot-daemon ot-ctl" script/cmake-build android-ndk
+        rm -rf build/ && OT_CMAKE_NINJA_TARGET="ot-cli" script/cmake-build android-ndk
diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml
index 5576dfd..88d6dea 100644
--- a/.github/workflows/codeql.yml
+++ b/.github/workflows/codeql.yml
@@ -54,7 +54,7 @@
 
     steps:
     - name: Harden Runner
-      uses: step-security/harden-runner@1f99358870fe1c846a3ccba386cc2b2246836776 # v2.2.1
+      uses: step-security/harden-runner@6b3083af2869dc3314a0257a42f4af696cc79ba3 # v2.3.1
       with:
         egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs
 
@@ -66,7 +66,7 @@
         sudo apt-get --no-install-recommends install -y ninja-build libreadline-dev libncurses-dev
 
     - name: Initialize CodeQL
-      uses: github/codeql-action/init@32dc499307d133bb5085bae78498c0ac2cf762d5 # v2.2.5
+      uses: github/codeql-action/init@29b1f65c5e92e24fe6b6647da1eaabe529cec70f # v2.3.3
       with:
         languages: ${{ matrix.language }}
         # If you wish to specify custom queries, you can do so here or in a config file.
@@ -80,6 +80,6 @@
         ./script/test build
 
     - name: Perform CodeQL Analysis
-      uses: github/codeql-action/analyze@32dc499307d133bb5085bae78498c0ac2cf762d5 # v2.2.5
+      uses: github/codeql-action/analyze@29b1f65c5e92e24fe6b6647da1eaabe529cec70f # v2.3.3
       with:
         category: "/language:${{matrix.language}}"
diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml
index d395bd1..46786d4 100644
--- a/.github/workflows/docker.yml
+++ b/.github/workflows/docker.yml
@@ -55,7 +55,7 @@
           - docker_name: environment
     steps:
     - name: Harden Runner
-      uses: step-security/harden-runner@1f99358870fe1c846a3ccba386cc2b2246836776 # v2.2.1
+      uses: step-security/harden-runner@6b3083af2869dc3314a0257a42f4af696cc79ba3 # v2.3.1
       with:
         egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs
 
@@ -73,14 +73,14 @@
 
         TAGS="--tag ${DOCKER_IMAGE}:${VERSION}"
 
-        echo ::set-output name=docker_image::${DOCKER_IMAGE}
-        echo ::set-output name=version::${VERSION}
-        echo ::set-output name=buildx_args::--platform ${DOCKER_PLATFORMS} \
+        echo "docker_image=${DOCKER_IMAGE}" >> $GITHUB_OUTPUT
+        echo "version=${VERSION}" >> $GITHUB_OUTPUT
+        echo "buildx_args=--platform ${DOCKER_PLATFORMS} \
           --build-arg OT_GIT_REF=${{ github.sha }} \
           --build-arg VERSION=${VERSION} \
           --build-arg BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ') \
           --build-arg VCS_REF=${GITHUB_SHA::8} \
-          ${TAGS} --file ${DOCKER_FILE} .
+          ${TAGS} --file ${DOCKER_FILE} ." >> $GITHUB_OUTPUT
 
     - name: Set up Docker Buildx
       uses: docker/setup-buildx-action@4b4e9c3e2d4531116a6f8ba8e71fc6e2cb6e6c8c # v2.5.0
diff --git a/.github/workflows/fuzz.yml b/.github/workflows/fuzz.yml
index a265ea8..e1b37fe 100644
--- a/.github/workflows/fuzz.yml
+++ b/.github/workflows/fuzz.yml
@@ -45,7 +45,7 @@
    runs-on: ubuntu-20.04
    steps:
    - name: Harden Runner
-     uses: step-security/harden-runner@1f99358870fe1c846a3ccba386cc2b2246836776 # v2.2.1
+     uses: step-security/harden-runner@6b3083af2869dc3314a0257a42f4af696cc79ba3 # v2.3.1
      with:
        egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs
 
diff --git a/.github/workflows/makefile-check.yml b/.github/workflows/makefile-check.yml
index 194488a..6972074 100644
--- a/.github/workflows/makefile-check.yml
+++ b/.github/workflows/makefile-check.yml
@@ -48,7 +48,7 @@
     runs-on: ubuntu-20.04
     steps:
     - name: Harden Runner
-      uses: step-security/harden-runner@1f99358870fe1c846a3ccba386cc2b2246836776 # v2.2.1
+      uses: step-security/harden-runner@6b3083af2869dc3314a0257a42f4af696cc79ba3 # v2.3.1
       with:
         egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs
 
diff --git a/.github/workflows/otbr.yml b/.github/workflows/otbr.yml
index fa0859c..9cab9ad 100644
--- a/.github/workflows/otbr.yml
+++ b/.github/workflows/otbr.yml
@@ -238,7 +238,7 @@
         script/test combine_coverage
     - name: Upload Coverage
       continue-on-error: true
-      uses: codecov/codecov-action@d9f34f8cd5cb3b3eb79b3e4b5dae3a16df499a70 # v3.1.1
+      uses: codecov/codecov-action@894ff025c7b54547a9a2a1e9f228beae737ad3c2 # v3.1.3
       with:
         files: final.info
         fail_ci_if_error: true
diff --git a/.github/workflows/otci.yml b/.github/workflows/otci.yml
index 78892df..46f0745 100644
--- a/.github/workflows/otci.yml
+++ b/.github/workflows/otci.yml
@@ -53,12 +53,11 @@
       matrix:
         virtual_time: [0, 1]
     env:
-      REFERENCE_DEVICE: 1
       VIRTUAL_TIME: ${{ matrix.virtual_time }}
       REAL_DEVICE: 0
     steps:
     - name: Harden Runner
-      uses: step-security/harden-runner@1f99358870fe1c846a3ccba386cc2b2246836776 # v2.2.1
+      uses: step-security/harden-runner@6b3083af2869dc3314a0257a42f4af696cc79ba3 # v2.3.1
       with:
         egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs
 
@@ -66,17 +65,21 @@
     - name: Bootstrap
       run: |
         sudo rm /etc/apt/sources.list.d/* && sudo apt-get update
-        sudo apt-get --no-install-recommends install -y g++-multilib python3-setuptools python3-wheel
+        sudo apt-get --no-install-recommends install -y g++-multilib ninja-build python3-setuptools python3-wheel
         python3 -m pip install -r tests/scripts/thread-cert/requirements.txt
+        python3 -m pip install pytype adb-shell
+    - name: Style check
+      run: |
+        PYTHONPATH=./tests/scripts/thread-cert pytype tools/otci
     - name: Build
       run: |
-        ./bootstrap
-        make -f examples/Makefile-simulation THREAD_VERSION=1.3 DUA=1 MLR=1 BACKBONE_ROUTER=1 CSL_RECEIVER=1
+        ./script/cmake-build simulation -DOT_THREAD_VERSION=1.3 -DOT_DUA=ON -DOT_MLR=ON -DOT_BACKBONE_ROUTER=ON \
+        -DOT_CSL_RECEIVER=ON -DOT_SIMULATION_VIRTUAL_TIME=${VIRTUAL_TIME}
     - name: Install OTCI Python Library
       run: |
-        (cd tools/otci && python3 setup.py install --user)
+        (cd tools/otci && python3 -m pip install .)
     - name: Run
       run: |
         export PYTHONPATH=./tests/scripts/thread-cert/
-        export OT_CLI=./output/simulation/bin/ot-cli-ftd
+        export OT_CLI=./build/simulation/examples/apps/cli/ot-cli-ftd
         python3 tools/otci/tests/test_otci.py
diff --git a/.github/workflows/otns.yml b/.github/workflows/otns.yml
index 4c11999..f0a3424 100644
--- a/.github/workflows/otns.yml
+++ b/.github/workflows/otns.yml
@@ -58,16 +58,16 @@
     runs-on: ubuntu-22.04
     steps:
     - name: Harden Runner
-      uses: step-security/harden-runner@1f99358870fe1c846a3ccba386cc2b2246836776 # v2.2.1
+      uses: step-security/harden-runner@6b3083af2869dc3314a0257a42f4af696cc79ba3 # v2.3.1
       with:
         egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs
 
     - uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
-    - uses: actions/setup-go@4d34df0c2316fe8122ab82dc22947d607c0c91f9 # v4.0.0
+    - uses: actions/setup-go@fac708d6674e30b6ba41289acaab6d4b75aa0753 # v4.0.1
       with:
         go-version: "1.20"
     - name: Set up Python
-      uses: actions/setup-python@d27e3f3d7c64b4bbf8e4abfb9b63b83e846e0435 # v4.5.0
+      uses: actions/setup-python@57ded4d7d5e986d7296eab16560982c6dd7c923b # v4.6.0
       with:
         python-version: "3.9"
     - name: Bootstrap
@@ -103,11 +103,11 @@
     runs-on: ubuntu-22.04
     steps:
       - uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
-      - uses: actions/setup-go@4d34df0c2316fe8122ab82dc22947d607c0c91f9 # v4.0.0
+      - uses: actions/setup-go@fac708d6674e30b6ba41289acaab6d4b75aa0753 # v4.0.1
         with:
           go-version: "1.20"
       - name: Set up Python
-        uses: actions/setup-python@d27e3f3d7c64b4bbf8e4abfb9b63b83e846e0435 # v4.5.0
+        uses: actions/setup-python@57ded4d7d5e986d7296eab16560982c6dd7c923b # v4.6.0
         with:
           python-version: "3.9"
       - name: Bootstrap
@@ -161,16 +161,16 @@
       STRESS_LEVEL: ${{ matrix.stress_level }}
     steps:
       - name: Harden Runner
-        uses: step-security/harden-runner@1f99358870fe1c846a3ccba386cc2b2246836776 # v2.2.1
+        uses: step-security/harden-runner@6b3083af2869dc3314a0257a42f4af696cc79ba3 # v2.3.1
         with:
           egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs
 
       - uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
-      - uses: actions/setup-go@4d34df0c2316fe8122ab82dc22947d607c0c91f9 # v4.0.0
+      - uses: actions/setup-go@fac708d6674e30b6ba41289acaab6d4b75aa0753 # v4.0.1
         with:
           go-version: "1.20"
       - name: Set up Python
-        uses: actions/setup-python@d27e3f3d7c64b4bbf8e4abfb9b63b83e846e0435 # v4.5.0
+        uses: actions/setup-python@57ded4d7d5e986d7296eab16560982c6dd7c923b # v4.6.0
         with:
           python-version: "3.9"
       - name: Bootstrap
@@ -209,7 +209,7 @@
     runs-on: ubuntu-22.04
     steps:
       - name: Harden Runner
-        uses: step-security/harden-runner@1f99358870fe1c846a3ccba386cc2b2246836776 # v2.2.1
+        uses: step-security/harden-runner@6b3083af2869dc3314a0257a42f4af696cc79ba3 # v2.3.1
         with:
           egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs
 
diff --git a/.github/workflows/posix.yml b/.github/workflows/posix.yml
index 1b68e0d..ded9e81 100644
--- a/.github/workflows/posix.yml
+++ b/.github/workflows/posix.yml
@@ -52,7 +52,7 @@
       CXXFLAGS: -DCLI_COAP_SECURE_USE_COAP_DEFAULT_HANDLER=1 -DOPENTHREAD_CONFIG_MLE_MAX_CHILDREN=15
     steps:
     - name: Harden Runner
-      uses: step-security/harden-runner@1f99358870fe1c846a3ccba386cc2b2246836776 # v2.2.1
+      uses: step-security/harden-runner@6b3083af2869dc3314a0257a42f4af696cc79ba3 # v2.3.1
       with:
         egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs
 
@@ -131,14 +131,11 @@
     env:
       COVERAGE: 1
       PYTHONUNBUFFERED: 1
-      READLINE: readline
-      REFERENCE_DEVICE: 1
       THREAD_VERSION: 1.1
       VIRTUAL_TIME: 1
-      VIRTUAL_TIME_UART: 1
     steps:
     - name: Harden Runner
-      uses: step-security/harden-runner@1f99358870fe1c846a3ccba386cc2b2246836776 # v2.2.1
+      uses: step-security/harden-runner@6b3083af2869dc3314a0257a42f4af696cc79ba3 # v2.3.1
       with:
         egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs
 
@@ -148,21 +145,19 @@
     - name: Bootstrap
       run: |
         sudo rm /etc/apt/sources.list.d/* && sudo apt-get update
-        sudo apt-get --no-install-recommends install -y libreadline6-dev python3-setuptools python3-wheel lcov
+        sudo apt-get --no-install-recommends install -y lcov ninja-build python3-setuptools python3-wheel
         python3 -m pip install -r tests/scripts/thread-cert/requirements.txt
     - name: Build
       run: |
-        ./bootstrap
-        make -f examples/Makefile-simulation
-        make -f src/posix/Makefile-posix
+        OT_NODE_TYPE=rcp ./script/test build
     - name: Run
       run: |
-        VERBOSE=1 OT_CLI_PATH="$PWD/output/posix/bin/ot-cli -v" RADIO_DEVICE="$PWD/output/simulation/bin/ot-rcp" make -f src/posix/Makefile-posix check
+        MAX_JOBS=$(getconf _NPROCESSORS_ONLN) ./script/test cert_suite ./tests/scripts/thread-cert/Cert_*.py ./tests/scripts/thread-cert/test_*.py
     - uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
       if: ${{ failure() }}
       with:
         name: thread-cert
-        path: build/posix/tests/scripts/thread-cert
+        path: ot_testing
     - name: Generate Coverage
       run: |
         ./script/test generate_coverage gcc
@@ -177,7 +172,7 @@
       COVERAGE: 1
     steps:
     - name: Harden Runner
-      uses: step-security/harden-runner@1f99358870fe1c846a3ccba386cc2b2246836776 # v2.2.1
+      uses: step-security/harden-runner@6b3083af2869dc3314a0257a42f4af696cc79ba3 # v2.3.1
       with:
         egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs
 
@@ -216,7 +211,7 @@
       OT_READLINE: 'readline'
     steps:
     - name: Harden Runner
-      uses: step-security/harden-runner@1f99358870fe1c846a3ccba386cc2b2246836776 # v2.2.1
+      uses: step-security/harden-runner@6b3083af2869dc3314a0257a42f4af696cc79ba3 # v2.3.1
       with:
         egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs
 
@@ -265,7 +260,7 @@
       OT_READLINE: 'off'
     steps:
     - name: Harden Runner
-      uses: step-security/harden-runner@1f99358870fe1c846a3ccba386cc2b2246836776 # v2.2.1
+      uses: step-security/harden-runner@6b3083af2869dc3314a0257a42f4af696cc79ba3 # v2.3.1
       with:
         egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs
 
@@ -295,7 +290,7 @@
     runs-on: ubuntu-20.04
     steps:
     - name: Harden Runner
-      uses: step-security/harden-runner@1f99358870fe1c846a3ccba386cc2b2246836776 # v2.2.1
+      uses: step-security/harden-runner@6b3083af2869dc3314a0257a42f4af696cc79ba3 # v2.3.1
       with:
         egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs
 
@@ -329,7 +324,7 @@
     runs-on: ubuntu-20.04
     steps:
     - name: Harden Runner
-      uses: step-security/harden-runner@1f99358870fe1c846a3ccba386cc2b2246836776 # v2.2.1
+      uses: step-security/harden-runner@6b3083af2869dc3314a0257a42f4af696cc79ba3 # v2.3.1
       with:
         egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs
 
@@ -346,7 +341,7 @@
       run: |
         script/test combine_coverage
     - name: Upload Coverage
-      uses: codecov/codecov-action@d9f34f8cd5cb3b3eb79b3e4b5dae3a16df499a70 # v3.1.1
+      uses: codecov/codecov-action@894ff025c7b54547a9a2a1e9f228beae737ad3c2 # v3.1.3
       with:
         files: final.info
         fail_ci_if_error: true
@@ -356,7 +351,7 @@
     runs-on: ubuntu-20.04
     steps:
     - name: Harden Runner
-      uses: step-security/harden-runner@1f99358870fe1c846a3ccba386cc2b2246836776 # v2.2.1
+      uses: step-security/harden-runner@6b3083af2869dc3314a0257a42f4af696cc79ba3 # v2.3.1
       with:
         egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs
 
diff --git a/.github/workflows/scorecards.yml b/.github/workflows/scorecards.yml
index 738dc67..5a241f4 100644
--- a/.github/workflows/scorecards.yml
+++ b/.github/workflows/scorecards.yml
@@ -65,7 +65,7 @@
           persist-credentials: false
 
       - name: "Run analysis"
-        uses: ossf/scorecard-action@e38b1902ae4f44df626f11ba0734b14fb91f8f86 # v2.1.2
+        uses: ossf/scorecard-action@80e868c13c90f172d68d1f4501dee99e2479f7af # v2.1.3
         with:
           results_file: results.sarif
           results_format: sarif
@@ -95,6 +95,6 @@
 
       # Upload the results to GitHub's code scanning dashboard.
       - name: "Upload to code-scanning"
-        uses: github/codeql-action/upload-sarif@32dc499307d133bb5085bae78498c0ac2cf762d5 # v2.1.27
+        uses: github/codeql-action/upload-sarif@29b1f65c5e92e24fe6b6647da1eaabe529cec70f # v2.1.27
         with:
           sarif_file: results.sarif
diff --git a/.github/workflows/simulation-1.1.yml b/.github/workflows/simulation-1.1.yml
index 485b418..d996c0e 100644
--- a/.github/workflows/simulation-1.1.yml
+++ b/.github/workflows/simulation-1.1.yml
@@ -55,7 +55,7 @@
       VIRTUAL_TIME: 1
     steps:
     - name: Harden Runner
-      uses: step-security/harden-runner@1f99358870fe1c846a3ccba386cc2b2246836776 # v2.2.1
+      uses: step-security/harden-runner@6b3083af2869dc3314a0257a42f4af696cc79ba3 # v2.3.1
       with:
         egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs
 
@@ -86,7 +86,7 @@
       MULTIPLY: 3
     steps:
     - name: Harden Runner
-      uses: step-security/harden-runner@1f99358870fe1c846a3ccba386cc2b2246836776 # v2.2.1
+      uses: step-security/harden-runner@6b3083af2869dc3314a0257a42f4af696cc79ba3 # v2.3.1
       with:
         egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs
 
@@ -134,7 +134,7 @@
       VIRTUAL_TIME: 1
     steps:
     - name: Harden Runner
-      uses: step-security/harden-runner@1f99358870fe1c846a3ccba386cc2b2246836776 # v2.2.1
+      uses: step-security/harden-runner@6b3083af2869dc3314a0257a42f4af696cc79ba3 # v2.3.1
       with:
         egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs
 
@@ -144,20 +144,19 @@
     - name: Bootstrap
       run: |
         sudo rm /etc/apt/sources.list.d/* && sudo apt-get update
-        sudo apt-get --no-install-recommends install -y g++-multilib python3-setuptools python3-wheel lcov
+        sudo apt-get --no-install-recommends install -y lcov ninja-build g++-multilib python3-setuptools python3-wheel
         python3 -m pip install -r tests/scripts/thread-cert/requirements.txt
     - name: Build
       run: |
-        ./bootstrap
-        make -f examples/Makefile-simulation
+        ./script/test build
     - name: Run
       run: |
-        VERBOSE=1 make -f examples/Makefile-simulation check
+        ./script/test cert_suite ./tests/scripts/thread-cert/Cert_*.py ./tests/scripts/thread-cert/test_*.py
     - uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
       if: ${{ failure() }}
       with:
         name: cli-ftd-thread-cert
-        path: build/simulation/tests/scripts/thread-cert
+        path: ot_testing
     - name: Generate Coverage
       run: |
         ./script/test generate_coverage gcc
@@ -185,7 +184,7 @@
       MESSAGE_USE_HEAP: ${{ matrix.message_use_heap }}
     steps:
     - name: Harden Runner
-      uses: step-security/harden-runner@1f99358870fe1c846a3ccba386cc2b2246836776 # v2.2.1
+      uses: step-security/harden-runner@6b3083af2869dc3314a0257a42f4af696cc79ba3 # v2.3.1
       with:
         egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs
 
@@ -195,20 +194,19 @@
     - name: Bootstrap
       run: |
         sudo rm /etc/apt/sources.list.d/* && sudo apt-get update
-        sudo apt-get --no-install-recommends install -y g++-multilib python3-setuptools python3-wheel lcov
+        sudo apt-get --no-install-recommends install -y lcov ninja-build g++-multilib python3-setuptools python3-wheel
         python3 -m pip install -r tests/scripts/thread-cert/requirements.txt
     - name: Build
       run: |
-        ./bootstrap
-        make -f examples/Makefile-simulation
+        ./script/test build
     - name: Run
       run: |
-        VERBOSE=1 make -f examples/Makefile-simulation check
+        ./script/test cert_suite ./tests/scripts/thread-cert/Cert_*.py ./tests/scripts/thread-cert/test_*.py
     - uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
       if: ${{ failure() }}
       with:
         name: cli-mtd-thread-cert
-        path: build/simulation/tests/scripts/thread-cert
+        path: ot_testing
     - name: Generate Coverage
       run: |
         ./script/test generate_coverage gcc
@@ -226,11 +224,10 @@
       COVERAGE: 1
       REFERENCE_DEVICE: 1
       THREAD_VERSION: 1.1
-      TIME_SYNC: 1
       VIRTUAL_TIME: 1
     steps:
     - name: Harden Runner
-      uses: step-security/harden-runner@1f99358870fe1c846a3ccba386cc2b2246836776 # v2.2.1
+      uses: step-security/harden-runner@6b3083af2869dc3314a0257a42f4af696cc79ba3 # v2.3.1
       with:
         egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs
 
@@ -240,20 +237,19 @@
     - name: Bootstrap
       run: |
         sudo rm /etc/apt/sources.list.d/* && sudo apt-get update
-        sudo apt-get --no-install-recommends install -y g++-multilib python3-setuptools python3-wheel lcov
+        sudo apt-get --no-install-recommends install -y g++-multilib lcov ninja-build python3-setuptools python3-wheel
         python3 -m pip install -r tests/scripts/thread-cert/requirements.txt
     - name: Build
       run: |
-        ./bootstrap
-        make -f examples/Makefile-simulation
+        OT_OPTIONS="-DOT_TIME_SYNC=ON" ./script/test build
     - name: Run
       run: |
-        VERBOSE=1 make -f examples/Makefile-simulation check
+        ./script/test cert_suite ./tests/scripts/thread-cert/Cert_*.py ./tests/scripts/thread-cert/test_*.py
     - uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
       if: ${{ failure() }}
       with:
         name: cli-time-sync-thread-cert
-        path: build/simulation/tests/scripts/thread-cert
+        path: ot_testing
     - name: Generate Coverage
       run: |
         ./script/test generate_coverage gcc
@@ -270,7 +266,7 @@
       THREAD_VERSION: 1.1
     steps:
     - name: Harden Runner
-      uses: step-security/harden-runner@1f99358870fe1c846a3ccba386cc2b2246836776 # v2.2.1
+      uses: step-security/harden-runner@6b3083af2869dc3314a0257a42f4af696cc79ba3 # v2.3.1
       with:
         egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs
 
@@ -309,7 +305,7 @@
       THREAD_VERSION: 1.1
     steps:
     - name: Harden Runner
-      uses: step-security/harden-runner@1f99358870fe1c846a3ccba386cc2b2246836776 # v2.2.1
+      uses: step-security/harden-runner@6b3083af2869dc3314a0257a42f4af696cc79ba3 # v2.3.1
       with:
         egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs
 
@@ -355,14 +351,12 @@
     runs-on: ubuntu-20.04
     env:
       COVERAGE: 1
-      MULTIPLE_INSTANCE: 1
-      REFERENCE_DEVICE: 1
       THREAD_VERSION: 1.1
       VIRTUAL_TIME: 1
       CXXFLAGS: "-DOPENTHREAD_CONFIG_LOG_PREPEND_UPTIME=0"
     steps:
     - name: Harden Runner
-      uses: step-security/harden-runner@1f99358870fe1c846a3ccba386cc2b2246836776 # v2.2.1
+      uses: step-security/harden-runner@6b3083af2869dc3314a0257a42f4af696cc79ba3 # v2.3.1
       with:
         egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs
 
@@ -371,19 +365,18 @@
         submodules: true
     - name: Bootstrap
       run: |
-        sudo apt-get --no-install-recommends install -y python3-setuptools python3-wheel lcov
+        sudo apt-get --no-install-recommends install -y lcov ninja-build python3-setuptools python3-wheel
         python3 -m pip install -r tests/scripts/thread-cert/requirements.txt
     - name: Build
       run: |
-        ./bootstrap
-        make -f examples/Makefile-simulation
+        OT_OPTIONS="-DOT_MULTIPLE_INSTANCE=ON" ./script/test build
     - name: Run
       run: |
-        VERBOSE=1 make -f examples/Makefile-simulation check
+        ./script/test cert_suite ./tests/scripts/thread-cert/Cert_*.py ./tests/scripts/thread-cert/test_*.py
     - uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
       if: ${{ failure() }}
       with:
-        name: multiple-instance-thread-cert
+        name: ot_testing
         path: build/simulation/tests/scripts/thread-cert
     - name: Generate Coverage
       run: |
@@ -405,7 +398,7 @@
     runs-on: ubuntu-20.04
     steps:
     - name: Harden Runner
-      uses: step-security/harden-runner@1f99358870fe1c846a3ccba386cc2b2246836776 # v2.2.1
+      uses: step-security/harden-runner@6b3083af2869dc3314a0257a42f4af696cc79ba3 # v2.3.1
       with:
         egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs
 
@@ -422,7 +415,7 @@
       run: |
         script/test combine_coverage
     - name: Upload Coverage
-      uses: codecov/codecov-action@d9f34f8cd5cb3b3eb79b3e4b5dae3a16df499a70 # v3.1.1
+      uses: codecov/codecov-action@894ff025c7b54547a9a2a1e9f228beae737ad3c2 # v3.1.3
       with:
         files: final.info
         fail_ci_if_error: true
@@ -432,7 +425,7 @@
     runs-on: ubuntu-20.04
     steps:
     - name: Harden Runner
-      uses: step-security/harden-runner@1f99358870fe1c846a3ccba386cc2b2246836776 # v2.2.1
+      uses: step-security/harden-runner@6b3083af2869dc3314a0257a42f4af696cc79ba3 # v2.3.1
       with:
         egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs
 
diff --git a/.github/workflows/simulation-1.2.yml b/.github/workflows/simulation-1.2.yml
index 658ff8c..cbf2c9e 100644
--- a/.github/workflows/simulation-1.2.yml
+++ b/.github/workflows/simulation-1.2.yml
@@ -56,6 +56,7 @@
       THREAD_VERSION: 1.3
       VIRTUAL_TIME: 1
       INTER_OP: 1
+      INTER_OP_BBR: 1
       CC: ${{ matrix.compiler.c }}
       CXX: ${{ matrix.compiler.cxx }}
     strategy:
@@ -65,7 +66,7 @@
         arch: ["m32", "m64"]
     steps:
     - name: Harden Runner
-      uses: step-security/harden-runner@1f99358870fe1c846a3ccba386cc2b2246836776 # v2.2.1
+      uses: step-security/harden-runner@6b3083af2869dc3314a0257a42f4af696cc79ba3 # v2.3.1
       with:
         egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs
 
@@ -126,7 +127,7 @@
       INTER_OP_BBR: 0
     steps:
     - name: Harden Runner
-      uses: step-security/harden-runner@1f99358870fe1c846a3ccba386cc2b2246836776 # v2.2.1
+      uses: step-security/harden-runner@6b3083af2869dc3314a0257a42f4af696cc79ba3 # v2.3.1
       with:
         egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs
 
@@ -186,10 +187,11 @@
       VIRTUAL_TIME: 1
       PACKET_VERIFICATION: 1
       THREAD_VERSION: 1.3
+      INTER_OP_BBR: 1
       MULTIPLY: 3
     steps:
     - name: Harden Runner
-      uses: step-security/harden-runner@1f99358870fe1c846a3ccba386cc2b2246836776 # v2.2.1
+      uses: step-security/harden-runner@6b3083af2869dc3314a0257a42f4af696cc79ba3 # v2.3.1
       with:
         egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs
 
@@ -233,7 +235,7 @@
       VIRTUAL_TIME: 0
     steps:
     - name: Harden Runner
-      uses: step-security/harden-runner@1f99358870fe1c846a3ccba386cc2b2246836776 # v2.2.1
+      uses: step-security/harden-runner@6b3083af2869dc3314a0257a42f4af696cc79ba3 # v2.3.1
       with:
         egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs
 
@@ -281,7 +283,7 @@
       INTER_OP: 1
     steps:
     - name: Harden Runner
-      uses: step-security/harden-runner@1f99358870fe1c846a3ccba386cc2b2246836776 # v2.2.1
+      uses: step-security/harden-runner@6b3083af2869dc3314a0257a42f4af696cc79ba3 # v2.3.1
       with:
         egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs
 
@@ -341,7 +343,7 @@
     runs-on: ubuntu-20.04
     steps:
     - name: Harden Runner
-      uses: step-security/harden-runner@1f99358870fe1c846a3ccba386cc2b2246836776 # v2.2.1
+      uses: step-security/harden-runner@6b3083af2869dc3314a0257a42f4af696cc79ba3 # v2.3.1
       with:
         egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs
 
@@ -358,7 +360,7 @@
       run: |
         script/test combine_coverage
     - name: Upload Coverage
-      uses: codecov/codecov-action@d9f34f8cd5cb3b3eb79b3e4b5dae3a16df499a70 # v3.1.1
+      uses: codecov/codecov-action@894ff025c7b54547a9a2a1e9f228beae737ad3c2 # v3.1.3
       with:
         files: final.info
         fail_ci_if_error: true
@@ -368,7 +370,7 @@
     runs-on: ubuntu-20.04
     steps:
     - name: Harden Runner
-      uses: step-security/harden-runner@1f99358870fe1c846a3ccba386cc2b2246836776 # v2.2.1
+      uses: step-security/harden-runner@6b3083af2869dc3314a0257a42f4af696cc79ba3 # v2.3.1
       with:
         egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs
 
diff --git a/.github/workflows/size.yml b/.github/workflows/size.yml
index 5742e57..71afb0d 100644
--- a/.github/workflows/size.yml
+++ b/.github/workflows/size.yml
@@ -49,7 +49,7 @@
     runs-on: ubuntu-20.04
     steps:
     - name: Harden Runner
-      uses: step-security/harden-runner@1f99358870fe1c846a3ccba386cc2b2246836776 # v2.2.1
+      uses: step-security/harden-runner@6b3083af2869dc3314a0257a42f4af696cc79ba3 # v2.3.1
       with:
         egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs
 
diff --git a/.github/workflows/toranj.yml b/.github/workflows/toranj.yml
index 4271263..83cf712 100644
--- a/.github/workflows/toranj.yml
+++ b/.github/workflows/toranj.yml
@@ -59,7 +59,7 @@
       TORANJ_EVENT_NAME: ${{ github.event_name }}
     steps:
     - name: Harden Runner
-      uses: step-security/harden-runner@1f99358870fe1c846a3ccba386cc2b2246836776 # v2.2.1
+      uses: step-security/harden-runner@6b3083af2869dc3314a0257a42f4af696cc79ba3 # v2.3.1
       with:
         egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs
 
@@ -88,7 +88,7 @@
       TORANJ_CLI: 1
     steps:
     - name: Harden Runner
-      uses: step-security/harden-runner@1f99358870fe1c846a3ccba386cc2b2246836776 # v2.2.1
+      uses: step-security/harden-runner@6b3083af2869dc3314a0257a42f4af696cc79ba3 # v2.3.1
       with:
         egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs
 
@@ -120,7 +120,7 @@
     runs-on: ubuntu-20.04
     steps:
     - name: Harden Runner
-      uses: step-security/harden-runner@1f99358870fe1c846a3ccba386cc2b2246836776 # v2.2.1
+      uses: step-security/harden-runner@6b3083af2869dc3314a0257a42f4af696cc79ba3 # v2.3.1
       with:
         egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs
 
@@ -147,7 +147,7 @@
     runs-on: ubuntu-20.04
     steps:
     - name: Harden Runner
-      uses: step-security/harden-runner@1f99358870fe1c846a3ccba386cc2b2246836776 # v2.2.1
+      uses: step-security/harden-runner@6b3083af2869dc3314a0257a42f4af696cc79ba3 # v2.3.1
       with:
         egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs
 
@@ -164,7 +164,7 @@
       run: |
         script/test combine_coverage
     - name: Upload Coverage
-      uses: codecov/codecov-action@d9f34f8cd5cb3b3eb79b3e4b5dae3a16df499a70 # v3.1.1
+      uses: codecov/codecov-action@894ff025c7b54547a9a2a1e9f228beae737ad3c2 # v3.1.3
       with:
         files: final.info
         fail_ci_if_error: true
@@ -174,7 +174,7 @@
     runs-on: ubuntu-20.04
     steps:
     - name: Harden Runner
-      uses: step-security/harden-runner@1f99358870fe1c846a3ccba386cc2b2246836776 # v2.2.1
+      uses: step-security/harden-runner@6b3083af2869dc3314a0257a42f4af696cc79ba3 # v2.3.1
       with:
         egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs
 
diff --git a/.github/workflows/unit.yml b/.github/workflows/unit.yml
index c820d74..a92c629 100644
--- a/.github/workflows/unit.yml
+++ b/.github/workflows/unit.yml
@@ -49,7 +49,7 @@
     runs-on: ubuntu-20.04
     steps:
     - name: Harden Runner
-      uses: step-security/harden-runner@1f99358870fe1c846a3ccba386cc2b2246836776 # v2.2.1
+      uses: step-security/harden-runner@6b3083af2869dc3314a0257a42f4af696cc79ba3 # v2.3.1
       with:
         egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs
 
@@ -67,7 +67,7 @@
       COVERAGE: 1
     steps:
     - name: Harden Runner
-      uses: step-security/harden-runner@1f99358870fe1c846a3ccba386cc2b2246836776 # v2.2.1
+      uses: step-security/harden-runner@6b3083af2869dc3314a0257a42f4af696cc79ba3 # v2.3.1
       with:
         egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs
 
@@ -100,7 +100,7 @@
     runs-on: ubuntu-20.04
     steps:
     - name: Harden Runner
-      uses: step-security/harden-runner@1f99358870fe1c846a3ccba386cc2b2246836776 # v2.2.1
+      uses: step-security/harden-runner@6b3083af2869dc3314a0257a42f4af696cc79ba3 # v2.3.1
       with:
         egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs
 
@@ -117,7 +117,7 @@
       run: |
         script/test combine_coverage
     - name: Upload Coverage
-      uses: codecov/codecov-action@d9f34f8cd5cb3b3eb79b3e4b5dae3a16df499a70 # v3.1.1
+      uses: codecov/codecov-action@894ff025c7b54547a9a2a1e9f228beae737ad3c2 # v3.1.3
       with:
         files: final.info
         fail_ci_if_error: true
@@ -127,7 +127,7 @@
     runs-on: ubuntu-20.04
     steps:
     - name: Harden Runner
-      uses: step-security/harden-runner@1f99358870fe1c846a3ccba386cc2b2246836776 # v2.2.1
+      uses: step-security/harden-runner@6b3083af2869dc3314a0257a42f4af696cc79ba3 # v2.3.1
       with:
         egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs
 
diff --git a/.github/workflows/version.yml b/.github/workflows/version.yml
index 9a924d9..47828d3 100644
--- a/.github/workflows/version.yml
+++ b/.github/workflows/version.yml
@@ -45,7 +45,7 @@
     runs-on: ubuntu-20.04
     steps:
     - name: Harden Runner
-      uses: step-security/harden-runner@1f99358870fe1c846a3ccba386cc2b2246836776 # v2.2.1
+      uses: step-security/harden-runner@6b3083af2869dc3314a0257a42f4af696cc79ba3 # v2.3.1
       with:
         egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs
 
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 380f0fb..15659e8 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -175,6 +175,7 @@
 
 if(OT_PLATFORM STREQUAL "posix")
     target_include_directories(ot-config INTERFACE ${PROJECT_SOURCE_DIR}/src/posix/platform)
+    target_compile_definitions(ot-config INTERFACE OPENTHREAD_PLATFORM_POSIX=1)
     add_subdirectory("${PROJECT_SOURCE_DIR}/src/posix/platform")
 elseif(OT_PLATFORM STREQUAL "external")
     # skip in this case
diff --git a/configure.ac b/configure.ac
index 798949d..dedd85d 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1029,8 +1029,6 @@
 tools/spi-hdlc-adapter/Makefile
 tests/Makefile
 tests/fuzz/Makefile
-tests/scripts/Makefile
-tests/scripts/thread-cert/Makefile
 doc/Makefile
 ])
 
diff --git a/doc/Doxyfile.in b/doc/Doxyfile.in
index 014224b..0fa2af8 100644
--- a/doc/Doxyfile.in
+++ b/doc/Doxyfile.in
@@ -1144,7 +1144,7 @@
 # defined cascading style sheet that is included after the standard style sheets
 # created by doxygen. Using this option one can overrule certain style aspects.
 # This is preferred over using HTML_STYLESHEET since it does not replace the
-# standard style sheet and is therefor more robust against future updates.
+# standard style sheet and is therefore more robust against future updates.
 # Doxygen will copy the style sheet file to the output directory. For an example
 # see the documentation.
 # This tag requires that the tag GENERATE_HTML is set to YES.
@@ -2029,7 +2029,7 @@
 EXPAND_AS_DEFINED      =
 
 # If the SKIP_FUNCTION_MACROS tag is set to YES then doxygen's preprocessor will
-# remove all refrences to function-like macros that are alone on a line, have an
+# remove all references to function-like macros that are alone on a line, have an
 # all uppercase name, and do not end with a semicolon. Such function macros are
 # typically used for boiler-plate code, and will confuse the parser if not
 # removed.
diff --git a/etc/cmake/options.cmake b/etc/cmake/options.cmake
index 7bbe4eb..6afef68 100644
--- a/etc/cmake/options.cmake
+++ b/etc/cmake/options.cmake
@@ -34,6 +34,8 @@
 option(OT_MTD "enable MTD" ON)
 option(OT_RCP "enable RCP" ON)
 
+option(OT_LINKER_MAP "generate .map files for example apps" ON)
+
 message(STATUS OT_APP_CLI=${OT_APP_CLI})
 message(STATUS OT_APP_NCP=${OT_APP_NCP})
 message(STATUS OT_APP_RCP=${OT_APP_RCP})
@@ -77,12 +79,14 @@
 endmacro()
 
 ot_option(OT_15_4 OPENTHREAD_CONFIG_RADIO_LINK_IEEE_802_15_4_ENABLE "802.15.4 radio link")
+ot_option(OT_ANDROID_NDK OPENTHREAD_CONFIG_ANDROID_NDK_ENABLE "enable android NDK")
 ot_option(OT_ANYCAST_LOCATOR OPENTHREAD_CONFIG_TMF_ANYCAST_LOCATOR_ENABLE "anycast locator")
 ot_option(OT_ASSERT OPENTHREAD_CONFIG_ASSERT_ENABLE "assert function OT_ASSERT()")
 ot_option(OT_BACKBONE_ROUTER OPENTHREAD_CONFIG_BACKBONE_ROUTER_ENABLE "backbone router functionality")
 ot_option(OT_BACKBONE_ROUTER_DUA_NDPROXYING OPENTHREAD_CONFIG_BACKBONE_ROUTER_DUA_NDPROXYING_ENABLE "BBR DUA ND Proxy")
 ot_option(OT_BACKBONE_ROUTER_MULTICAST_ROUTING OPENTHREAD_CONFIG_BACKBONE_ROUTER_MULTICAST_ROUTING_ENABLE "BBR MR")
 ot_option(OT_BORDER_AGENT OPENTHREAD_CONFIG_BORDER_AGENT_ENABLE "border agent")
+ot_option(OT_BORDER_AGENT_ID OPENTHREAD_CONFIG_BORDER_AGENT_ID_ENABLE "create and save border agent ID")
 ot_option(OT_BORDER_ROUTER OPENTHREAD_CONFIG_BORDER_ROUTER_ENABLE "border router")
 ot_option(OT_BORDER_ROUTING OPENTHREAD_CONFIG_BORDER_ROUTING_ENABLE "border routing")
 ot_option(OT_BORDER_ROUTING_COUNTERS OPENTHREAD_CONFIG_IP6_BR_COUNTERS_ENABLE "border routing counters")
@@ -122,12 +126,12 @@
 ot_option(OT_MESSAGE_USE_HEAP OPENTHREAD_CONFIG_MESSAGE_USE_HEAP_ENABLE "heap allocator for message buffers")
 ot_option(OT_MLE_LONG_ROUTES OPENTHREAD_CONFIG_MLE_LONG_ROUTES_ENABLE "MLE long routes extension (experimental)")
 ot_option(OT_MLR OPENTHREAD_CONFIG_MLR_ENABLE "Multicast Listener Registration (MLR)")
-ot_option(OT_MTD_NETDIAG OPENTHREAD_CONFIG_TMF_NETWORK_DIAG_MTD_ENABLE "TMF network diagnostics on MTDs")
 ot_option(OT_MULTIPLE_INSTANCE OPENTHREAD_CONFIG_MULTIPLE_INSTANCE_ENABLE "multiple instances")
 ot_option(OT_NAT64_BORDER_ROUTING OPENTHREAD_CONFIG_NAT64_BORDER_ROUTING_ENABLE "border routing NAT64")
 ot_option(OT_NAT64_TRANSLATOR OPENTHREAD_CONFIG_NAT64_TRANSLATOR_ENABLE "NAT64 translator support")
 ot_option(OT_NEIGHBOR_DISCOVERY_AGENT OPENTHREAD_CONFIG_NEIGHBOR_DISCOVERY_AGENT_ENABLE "neighbor discovery agent")
 ot_option(OT_NETDATA_PUBLISHER OPENTHREAD_CONFIG_NETDATA_PUBLISHER_ENABLE "Network Data publisher")
+ot_option(OT_NETDIAG_CLIENT OPENTHREAD_CONFIG_TMF_NETDIAG_CLIENT_ENABLE "Network Diagnostic client")
 ot_option(OT_OTNS OPENTHREAD_CONFIG_OTNS_ENABLE "OTNS")
 ot_option(OT_PING_SENDER OPENTHREAD_CONFIG_PING_SENDER_ENABLE "ping sender" ${OT_APP_CLI})
 ot_option(OT_PLATFORM_NETIF OPENTHREAD_CONFIG_PLATFORM_NETIF_ENABLE "platform netif")
@@ -157,6 +161,36 @@
     target_compile_definitions(ot-config INTERFACE "OPENTHREAD_CONFIG_LOG_PREPEND_LEVEL=1")
 endif()
 
+set(OT_VENDOR_NAME "" CACHE STRING "set the vendor name config")
+set_property(CACHE OT_VENDOR_NAME PROPERTY STRINGS ${OT_VENDOR_NAME_VALUES})
+string(COMPARE EQUAL "${OT_VENDOR_NAME}" "" is_empty)
+if (is_empty)
+    message(STATUS "OT_VENDOR_NAME=\"\"")
+else()
+    message(STATUS "OT_VENDOR_NAME=\"${OT_VENDOR_NAME}\" --> OPENTHREAD_CONFIG_NET_DIAG_VENDOR_NAME=\"${OT_VENDOR_NAME}\"")
+    target_compile_definitions(ot-config INTERFACE "OPENTHREAD_CONFIG_NET_DIAG_VENDOR_NAME=\"${OT_VENDOR_NAME}\"")
+endif()
+
+set(OT_VENDOR_MODEL "" CACHE STRING "set the vendor model config")
+set_property(CACHE OT_VENDOR_MODEL PROPERTY STRINGS ${OT_VENDOR_MODEL_VALUES})
+string(COMPARE EQUAL "${OT_VENDOR_MODEL}" "" is_empty)
+if (is_empty)
+    message(STATUS "OT_VENDOR_MODEL=\"\"")
+else()
+    message(STATUS "OT_VENDOR_MODEL=\"${OT_VENDOR_MODEL}\" --> OPENTHREAD_CONFIG_NET_DIAG_VENDOR_MODEL=\"${OT_VENDOR_MODEL}\"")
+    target_compile_definitions(ot-config INTERFACE "OPENTHREAD_CONFIG_NET_DIAG_VENDOR_MODEL=\"${OT_VENDOR_MODEL}\"")
+endif()
+
+set(OT_VENDOR_SW_VERSION "" CACHE STRING "set the vendor sw version config")
+set_property(CACHE OT_VENDOR_SW_VERSION PROPERTY STRINGS ${OT_VENDOR_SW_VERSION_VALUES})
+string(COMPARE EQUAL "${OT_VENDOR_SW_VERSION}" "" is_empty)
+if (is_empty)
+    message(STATUS "OT_VENDOR_SW_VERSION=\"\"")
+else()
+    message(STATUS "OT_VENDOR_SW_VERSION=\"${OT_VENDOR_SW_VERSION}\" --> OPENTHREAD_CONFIG_NET_DIAG_VENDOR_SW_VERSION=\"${OT_VENDOR_SW_VERSION}\"")
+    target_compile_definitions(ot-config INTERFACE "OPENTHREAD_CONFIG_NET_DIAG_VENDOR_SW_VERSION=\"${OT_VENDOR_SW_VERSION}\"")
+endif()
+
 set(OT_POWER_SUPPLY "" CACHE STRING "set the device power supply config")
 set(OT_POWER_SUPPLY_VALUES
     ""
@@ -190,8 +224,6 @@
     message(FATAL_ERROR "Invalid max RCP restoration count: ${OT_RCP_RESTORATION_MAX_COUNT}")
 endif()
 
-option(OT_EXCLUDE_TCPLP_LIB "exclude TCPlp library from build")
-
 if(NOT OT_EXTERNAL_MBEDTLS)
     set(OT_MBEDTLS mbedtls)
     target_compile_definitions(ot-config INTERFACE "OPENTHREAD_CONFIG_ENABLE_BUILTIN_MBEDTLS=1")
@@ -210,3 +242,18 @@
 if(OT_POSIX_SETTINGS_PATH)
     target_compile_definitions(ot-config INTERFACE "OPENTHREAD_CONFIG_POSIX_SETTINGS_PATH=${OT_POSIX_SETTINGS_PATH}")
 endif()
+
+#-----------------------------------------------------------------------------------------------------------------------
+# Check removed/replaced options
+
+macro(ot_removed_option name error)
+    # This macro checks for a remove option and emits an error
+    # if the option is set.
+    get_property(is_set CACHE ${name} PROPERTY VALUE SET)
+    if (is_set)
+        message(FATAL_ERROR "Removed option ${name} is set - ${error}")
+    endif()
+endmacro()
+
+ot_removed_option(OT_MTD_NETDIAG "- Use OT_NETDIAG_CLIENT instead - note that server function is always supported")
+ot_removed_option(OT_EXCLUDE_TCPLP_LIB "- Use OT_TCP instead, OT_EXCLUDE_TCPLP_LIB is deprecated")
diff --git a/etc/gn/openthread.gni b/etc/gn/openthread.gni
index 6afc0a8..8726edd 100644
--- a/etc/gn/openthread.gni
+++ b/etc/gn/openthread.gni
@@ -87,6 +87,9 @@
     # Enable border agent support
     openthread_config_border_agent_enable = false
 
+    # Enable border agent ID
+    openthread_config_border_agent_id_enable = false
+
     # Enable border router support
     openthread_config_border_router_enable = false
 
@@ -177,8 +180,8 @@
     # Enable MLE long routes extension (experimental, breaks Thread conformance]
     openthread_config_mle_long_routes_enable = false
 
-    # Enable TMF network diagnostics on MTDs
-    openthread_config_tmf_network_diag_mtd_enable = false
+    # Enable TMF network diagnostics client
+    openthread_config_tmf_netdiag_client_enable = false
 
     # Enable multiple instances
     openthread_config_multiple_instance_enable = false
diff --git a/examples/Makefile-simulation b/examples/Makefile-simulation
index f7577f7..72894fb 100644
--- a/examples/Makefile-simulation
+++ b/examples/Makefile-simulation
@@ -59,9 +59,9 @@
 JOINER                         ?= 1
 LINK_RAW                       ?= 1
 MAC_FILTER                     ?= 1
-MTD_NETDIAG                    ?= 1
 NEIGHBOR_DISCOVERY_AGENT       ?= 1
 NETDATA_PUBLISHER              ?= 1
+NETDIAG_CLIENT                 ?= 1
 PING_SENDER                    ?= 1
 REFERENCE_DEVICE               ?= 1
 SERVICE                        ?= 1
diff --git a/include/openthread/border_agent.h b/include/openthread/border_agent.h
index 83babfa..cfbcb56 100644
--- a/include/openthread/border_agent.h
+++ b/include/openthread/border_agent.h
@@ -52,6 +52,24 @@
  */
 
 /**
+ * The length of Border Agent/Router ID in bytes.
+ *
+ */
+#define OT_BORDER_AGENT_ID_LENGTH (16)
+
+/**
+ * @struct otBorderAgentId
+ *
+ * This structure represents a Border Agent ID.
+ *
+ */
+OT_TOOL_PACKED_BEGIN
+struct otBorderAgentId
+{
+    uint8_t mId[OT_BORDER_AGENT_ID_LENGTH];
+} OT_TOOL_PACKED_END;
+
+/**
  * This enumeration defines the Border Agent state.
  *
  */
@@ -83,6 +101,42 @@
 uint16_t otBorderAgentGetUdpPort(otInstance *aInstance);
 
 /**
+ * Gets the randomly generated Border Agent ID.
+ *
+ * The ID is saved in persistent storage and survives reboots. The typical use case of the ID is to
+ * be published in the MeshCoP mDNS service as the `id` TXT value for the client to identify this
+ * Border Router/Agent device.
+ *
+ * @param[in]    aInstance  A pointer to an OpenThread instance.
+ * @param[out]   aId        A pointer to buffer to receive the ID.
+ *
+ * @retval OT_ERROR_NONE  If successfully retrieved the Border Agent ID.
+ * @retval ...            If failed to retrieve the Border Agent ID.
+ *
+ * @sa otBorderAgentSetId
+ *
+ */
+otError otBorderAgentGetId(otInstance *aInstance, otBorderAgentId *aId);
+
+/**
+ * Sets the Border Agent ID.
+ *
+ * The Border Agent ID will be saved in persistent storage and survive reboots. It's required to
+ * set the ID only once after factory reset. If the ID has never been set by calling this function,
+ * a random ID will be generated and returned when `otBorderAgentGetId` is called.
+ *
+ * @param[in]    aInstance  A pointer to an OpenThread instance.
+ * @param[out]   aId        A pointer to the Border Agent ID.
+ *
+ * @retval OT_ERROR_NONE  If successfully set the Border Agent ID.
+ * @retval ...            If failed to set the Border Agent ID.
+ *
+ * @sa otBorderAgentGetId
+ *
+ */
+otError otBorderAgentSetId(otInstance *aInstance, const otBorderAgentId *aId);
+
+/**
  * @}
  *
  */
diff --git a/include/openthread/cli.h b/include/openthread/cli.h
index 4a80c9e..8d9a5f3 100644
--- a/include/openthread/cli.h
+++ b/include/openthread/cli.h
@@ -104,8 +104,10 @@
  * @param[in]  aLength        @p aUserCommands length.
  * @param[in]  aContext       @p The context passed to the handler.
  *
+ * @retval OT_ERROR_NONE    Successfully updated command table with commands from @p aUserCommands.
+ * @retval OT_ERROR_FAILED  Maximum number of command entries have already been set.
  */
-void otCliSetUserCommands(const otCliCommand *aUserCommands, uint8_t aLength, void *aContext);
+otError otCliSetUserCommands(const otCliCommand *aUserCommands, uint8_t aLength, void *aContext);
 
 /**
  * Write a number of bytes to the CLI console as a hex string.
@@ -147,6 +149,15 @@
 void otCliPlatLogv(otLogLevel aLogLevel, otLogRegion aLogRegion, const char *aFormat, va_list aArgs);
 
 /**
+ * Callback to allow vendor specific commands to be added to the user command table.
+ *
+ * Available when `OPENTHREAD_CONFIG_CLI_VENDOR_COMMANDS_ENABLE` is enabled and
+ * `OPENTHREAD_CONFIG_CLI_MAX_USER_CMD_ENTRIES` is greater than 1.
+ *
+ */
+extern void otCliVendorSetUserCommands(void);
+
+/**
  * @}
  *
  */
diff --git a/include/openthread/dataset.h b/include/openthread/dataset.h
index ac03436..efd18bc 100644
--- a/include/openthread/dataset.h
+++ b/include/openthread/dataset.h
@@ -229,7 +229,7 @@
 /**
  * This structure represents an Active or Pending Operational Dataset.
  *
- * Components in Dataset are optional. `mComponets` structure specifies which components are present in the Dataset.
+ * Components in Dataset are optional. `mComponents` structure specifies which components are present in the Dataset.
  *
  */
 typedef struct otOperationalDataset
diff --git a/include/openthread/dns.h b/include/openthread/dns.h
index 22f3fc2..e11855f 100644
--- a/include/openthread/dns.h
+++ b/include/openthread/dns.h
@@ -95,7 +95,7 @@
 } otDnsTxtEntry;
 
 /**
- * This structure represents an iterator for TXT record entires (key/value pairs).
+ * This structure represents an iterator for TXT record entries (key/value pairs).
  *
  * The data fields in this structure are intended for use by OpenThread core and caller should not read or change them.
  *
diff --git a/include/openthread/dns_client.h b/include/openthread/dns_client.h
index c7f25b7..6c9f5d1 100644
--- a/include/openthread/dns_client.h
+++ b/include/openthread/dns_client.h
@@ -81,6 +81,23 @@
 } otDnsNat64Mode;
 
 /**
+ * This enumeration type represents the service resolution mode in an `otDnsQueryConfig`.
+ *
+ * This is only used during DNS client service resolution `otDnsClientResolveService()`. It determines which
+ * record types to query.
+ *
+ */
+typedef enum
+{
+    OT_DNS_SERVICE_MODE_UNSPECIFIED      = 0, ///< Mode is not specified. Use default service mode.
+    OT_DNS_SERVICE_MODE_SRV              = 1, ///< Query for SRV record only.
+    OT_DNS_SERVICE_MODE_TXT              = 2, ///< Query for TXT record only.
+    OT_DNS_SERVICE_MODE_SRV_TXT          = 3, ///< Query for both SRV and TXT records in same message.
+    OT_DNS_SERVICE_MODE_SRV_TXT_SEPARATE = 4, ///< Query in parallel for SRV and TXT using separate messages.
+    OT_DNS_SERVICE_MODE_SRV_TXT_OPTIMIZE = 5, ///< Query for TXT/SRV together first, if fails then query separately.
+} otDnsServiceMode;
+
+/**
  * This enumeration type represents the DNS transport protocol in an `otDnsQueryConfig`.
  *
  * This `OT_DNS_TRANSPORT_TCP` is only supported when `OPENTHREAD_CONFIG_DNS_CLIENT_OVER_TCP_ENABLE` is enabled.
@@ -102,11 +119,12 @@
  */
 typedef struct otDnsQueryConfig
 {
-    otSockAddr          mServerSockAddr; ///< Server address (IPv6 address/port). All zero or zero port for unspecified.
+    otSockAddr          mServerSockAddr;  ///< Server address (IPv6 addr/port). All zero or zero port for unspecified.
     uint32_t            mResponseTimeout; ///< Wait time (in msec) to rx response. Zero indicates unspecified value.
     uint8_t             mMaxTxAttempts;   ///< Maximum tx attempts before reporting failure. Zero for unspecified value.
     otDnsRecursionFlag  mRecursionFlag;   ///< Indicates whether the server can resolve the query recursively or not.
     otDnsNat64Mode      mNat64Mode;       ///< Allow/Disallow NAT64 address translation during address resolution.
+    otDnsServiceMode    mServiceMode;     ///< Determines which records to query during service resolution.
     otDnsTransportProto mTransportProto;  ///< Select default transport protocol.
 } otDnsQueryConfig;
 
@@ -416,11 +434,12 @@
  *
  * This function MUST only be used from `otDnsBrowseCallback`.
  *
- * A browse DNS response should include the SRV, TXT, and AAAA records for the service instances that are enumerated
- * (note that it is a SHOULD and not a MUST requirement). This function tries to retrieve this info for a given service
- * instance when available.
+ * A browse DNS response can include SRV, TXT, and AAAA records for the service instances that are enumerated. This is
+ * a SHOULD and not a MUST requirement, and servers/resolvers are not required to provide this. This function attempts
+ * to retrieve this info for a given service instance when available.
  *
- * - If no matching SRV record is found in @p aResponse, `OT_ERROR_NOT_FOUND` is returned.
+ * - If no matching SRV record is found in @p aResponse, `OT_ERROR_NOT_FOUND` is returned. In this case, no additional
+ *   records (no TXT and/or AAAA) are read.
  * - If a matching SRV record is found in @p aResponse, @p aServiceInfo is updated and `OT_ERROR_NONE` is returned.
  * - If no matching TXT record is found in @p aResponse, `mTxtDataSize` in @p aServiceInfo is set to zero.
  * - If TXT data length is greater than `mTxtDataSize`, it is read partially and `mTxtDataTruncated` is set to true.
@@ -496,7 +515,7 @@
 typedef void (*otDnsServiceCallback)(otError aError, const otDnsServiceResponse *aResponse, void *aContext);
 
 /**
- * This function sends a DNS service instance resolution query for a given service instance.
+ * This function starts a DNS service instance resolution for a given service instance.
  *
  * This function is available when `OPENTHREAD_CONFIG_DNS_CLIENT_SERVICE_DISCOVERY_ENABLE` is enabled.
  *
@@ -504,6 +523,18 @@
  * the config for this query. In a non-NULL @p aConfig, some of the fields can be left unspecified (value zero). The
  * unspecified fields are then replaced by the values from the default config.
  *
+ * The function sends queries for SRV and/or TXT records for the given service instance. The `mServiceMode` field in
+ * `otDnsQueryConfig` determines which records to query (SRV only, TXT only, or both SRV and TXT) and how to perform
+ * the query (together in the same message, separately in parallel, or in optimized mode where client will try in the
+ * same message first and then separately if it fails to get a response).
+ *
+ * The SRV record provides information about service port, priority, and weight along with the host name associated
+ * with the service instance. This function DOES NOT perform address resolution for the host name discovered from SRV
+ * record. The server/resolver may provide AAAA/A record(s) for the host name in the Additional Data section of the
+ * response to SRV/TXT query and this information can be retrieved using `otDnsServiceResponseGetServiceInfo()` in
+ * `otDnsServiceCallback`. Users of this API MUST NOT assume that host address will always be available from
+ * `otDnsServiceResponseGetServiceInfo()`.
+ *
  * @param[in]  aInstance          A pointer to an OpenThread instance.
  * @param[in]  aInstanceLabel     The service instance label.
  * @param[in]  aServiceName       The service name (together with @p aInstanceLabel form full instance name).
@@ -524,6 +555,43 @@
                                   const otDnsQueryConfig *aConfig);
 
 /**
+ * This function starts a DNS service instance resolution for a given service instance, with a potential follow-up
+ * address resolution for the host name discovered for the service instance.
+ *
+ * This function is available when `OPENTHREAD_CONFIG_DNS_CLIENT_SERVICE_DISCOVERY_ENABLE` is enabled.
+ *
+ * The @p aConfig can be NULL. In this case the default config (from `otDnsClientGetDefaultConfig()`) will be used as
+ * the config for this query. In a non-NULL @p aConfig, some of the fields can be left unspecified (value zero). The
+ * unspecified fields are then replaced by the values from the default config. This function cannot be used with
+ * `mServiceMode` in DNS config set to `OT_DNS_SERVICE_MODE_TXT` (i.e., querying for TXT record only) and will return
+ * `OT_ERROR_INVALID_ARGS`.
+ *
+ * This function behaves similarly to `otDnsClientResolveService()` sending queries for SRV and TXT records. However,
+ * if the server/resolver does not provide AAAA/A records for the host name in the response to SRV query (in the
+ * Additional Data section), it will perform host name resolution (sending an AAAA query) for the discovered host name
+ * from the SRV record. The callback @p aCallback is invoked when responses for all queries are received (i.e., both
+ * service and host address resolutions are finished).
+ *
+ * @param[in]  aInstance          A pointer to an OpenThread instance.
+ * @param[in]  aInstanceLabel     The service instance label.
+ * @param[in]  aServiceName       The service name (together with @p aInstanceLabel form full instance name).
+ * @param[in]  aCallback          A function pointer that shall be called on response reception or time-out.
+ * @param[in]  aContext           A pointer to arbitrary context information.
+ * @param[in]  aConfig            A pointer to the config to use for this query.
+ *
+ * @retval OT_ERROR_NONE          Query sent successfully. @p aCallback will be invoked to report the status.
+ * @retval OT_ERROR_NO_BUFS       Insufficient buffer to prepare and send query.
+ * @retval OT_ERROR_INVALID_ARGS  @p aInstanceLabel is NULL, or @p aConfig is invalid.
+ *
+ */
+otError otDnsClientResolveServiceAndHostAddress(otInstance             *aInstance,
+                                                const char             *aInstanceLabel,
+                                                const char             *aServiceName,
+                                                otDnsServiceCallback    aCallback,
+                                                void                   *aContext,
+                                                const otDnsQueryConfig *aConfig);
+
+/**
  * This function gets the service instance name associated with a DNS service instance resolution response.
  *
  * This function MUST only be used from `otDnsServiceCallback`.
@@ -548,10 +616,21 @@
 /**
  * This function gets info for a service instance from a DNS service instance resolution response.
  *
- * This function MUST only be used from `otDnsServiceCallback`.
+ * This function MUST only be used from a `otDnsServiceCallback` triggered from `otDnsClientResolveService()` or
+ * `otDnsClientResolveServiceAndHostAddress()`.
  *
- * - If no matching SRV record is found in @p aResponse, `OT_ERROR_NOT_FOUND` is returned.
- * - If a matching SRV record is found in @p aResponse, @p aServiceInfo is updated and `OT_ERROR_NONE` is returned.
+ * When this is is used from a `otDnsClientResolveService()` callback, the DNS response from server/resolver may
+ * include AAAA records in its Additional Data section for the host name associated with the service instance that is
+ * resolved. This is a SHOULD and not a MUST requirement so servers/resolvers are not required to provide this. This
+ * function attempts to parse AAAA record(s) if included in the response. If it is not included `mHostAddress` is set
+ * to all zeros (unspecified address). To also resolve the host address, user can use the DNS client API function
+ * `otDnsClientResolveServiceAndHostAddress()` which will perform service resolution followed up by a host name
+ * address resolution query (when AAAA records are not provided by server/resolver in the SRV query response).
+ *
+ * - If a matching SRV record is found in @p aResponse, @p aServiceInfo is updated.
+ * - If no matching SRV record is found, `OT_ERROR_NOT_FOUND` is returned unless the query config for this query
+ *   used `OT_DNS_SERVICE_MODE_TXT` for `mServiceMode` (meaning the request was only for TXT record). In this case, we
+ *   still try to parse the SRV record from Additional Data Section of response (in case server provided the info).
  * - If no matching TXT record is found in @p aResponse, `mTxtDataSize` in @p aServiceInfo is set to zero.
  * - If TXT data length is greater than `mTxtDataSize`, it is read partially and `mTxtDataTruncated` is set to true.
  * - If no matching AAAA record is found in @p aResponse, `mHostAddress is set to all zero or unspecified address.
@@ -562,7 +641,7 @@
  * @param[out] aServiceInfo       A `ServiceInfo` to output the service instance information (MUST NOT be NULL).
  *
  * @retval OT_ERROR_NONE          The service instance info was read. @p aServiceInfo is updated.
- * @retval OT_ERROR_NOT_FOUND     Could not find a matching SRV record in @p aResponse.
+ * @retval OT_ERROR_NOT_FOUND     Could not find a required record in @p aResponse.
  * @retval OT_ERROR_NO_BUFS       The host name and/or TXT data could not fit in the given buffers.
  * @retval OT_ERROR_PARSE         Could not parse the records in the @p aResponse.
  *
diff --git a/include/openthread/history_tracker.h b/include/openthread/history_tracker.h
index 02d72f2..79a255a 100644
--- a/include/openthread/history_tracker.h
+++ b/include/openthread/history_tracker.h
@@ -43,7 +43,7 @@
  *   Records the history of different events, for example RX and TX messages or network info changes. All tracked
  *   entries are timestamped.
  *
- * The functions in this module are available when `OPENTHREAD_CONFIG_HISTOR_TRACKER_ENABLE` is enabled.
+ * The functions in this module are available when `OPENTHREAD_CONFIG_HISTORY_TRACKER_ENABLE` is enabled.
  *
  * @{
  *
@@ -152,7 +152,7 @@
     uint16_t   mChecksum;            ///< Message checksum (valid only for UDP/TCP/ICMP6).
     uint8_t    mIpProto;             ///< IP Protocol number (`OT_IP6_PROTO_*` enumeration).
     uint8_t    mIcmp6Type;           ///< ICMP6 type if msg is ICMP6, zero otherwise (`OT_ICMP6_TYPE_*` enumeration).
-    int8_t     mAveRxRss;            ///< RSS of received message or OT_RADIO_INVALI_RSSI if not known.
+    int8_t     mAveRxRss;            ///< RSS of received message or OT_RADIO_INVALID_RSSI if not known.
     bool       mLinkSecurity : 1;    ///< Indicates whether msg used link security.
     bool       mTxSuccess : 1;       ///< Indicates TX success (e.g., ack received). Applicable for TX msg only.
     uint8_t    mPriority : 2;        ///< Message priority (`OT_HISTORY_TRACKER_MSG_PRIORITY_*` enumeration).
diff --git a/include/openthread/instance.h b/include/openthread/instance.h
index f6eb33d..e317b9a 100644
--- a/include/openthread/instance.h
+++ b/include/openthread/instance.h
@@ -53,7 +53,7 @@
  * @note This number versions both OpenThread platform and user APIs.
  *
  */
-#define OPENTHREAD_API_VERSION (306)
+#define OPENTHREAD_API_VERSION (327)
 
 /**
  * @addtogroup api-instance
@@ -103,6 +103,17 @@
 otInstance *otInstanceInitSingle(void);
 
 /**
+ * Gets the instance identifier.
+ *
+ * The instance identifier is set to a random value when the instance is constructed, and then its value will not
+ * change after initialization.
+ *
+ * @returns The instance identifier.
+ *
+ */
+uint32_t otInstanceGetId(otInstance *aInstance);
+
+/**
  * This function indicates whether or not the instance is valid/initialized.
  *
  * The instance is considered valid if it is acquired and initialized using either `otInstanceInitSingle()` (in single
diff --git a/include/openthread/ip6.h b/include/openthread/ip6.h
index 5c38e31..c04bdc2 100644
--- a/include/openthread/ip6.h
+++ b/include/openthread/ip6.h
@@ -610,12 +610,27 @@
  * @param[in]   aString   A pointer to a NULL-terminated string.
  * @param[out]  aAddress  A pointer to an IPv6 address.
  *
- * @retval OT_ERROR_NONE          Successfully parsed the string.
- * @retval OT_ERROR_INVALID_ARGS  Failed to parse the string.
+ * @retval OT_ERROR_NONE   Successfully parsed @p aString and updated @p aAddress.
+ * @retval OT_ERROR_PARSE  Failed to parse @p aString as an IPv6 address.
  *
  */
 otError otIp6AddressFromString(const char *aString, otIp6Address *aAddress);
 
+/**
+ * This function converts a human-readable IPv6 prefix string into a binary representation.
+ *
+ * The @p aString parameter should be a string in the format "<address>/<plen>", where `<address>` is an IPv6
+ * address and `<plen>` is a prefix length.
+ *
+ * @param[in]   aString  A pointer to a NULL-terminated string.
+ * @param[out]  aPrefix  A pointer to an IPv6 prefix.
+ *
+ * @retval OT_ERROR_NONE   Successfully parsed the string as an IPv6 prefix and updated @p aPrefix.
+ * @retval OT_ERROR_PARSE  Failed to parse @p aString as an IPv6 prefix.
+ *
+ */
+otError otIp6PrefixFromString(const char *aString, otIp6Prefix *aPrefix);
+
 #define OT_IP6_ADDRESS_STRING_SIZE 40 ///< Recommended size for string representation of an IPv6 address.
 
 /**
diff --git a/include/openthread/link_raw.h b/include/openthread/link_raw.h
index 461b81d..1f43a05 100644
--- a/include/openthread/link_raw.h
+++ b/include/openthread/link_raw.h
@@ -236,7 +236,7 @@
  * @param[in]  aCallback        A pointer to a function called on completion of a scanned channel.
  *
  * @retval OT_ERROR_NONE             Successfully started scanning the channel.
- * @retval OT_ERROR_BUSY             The radio is performing enery scanning.
+ * @retval OT_ERROR_BUSY             The radio is performing energy scanning.
  * @retval OT_ERROR_NOT_IMPLEMENTED  The radio doesn't support energy scanning.
  * @retval OT_ERROR_INVALID_STATE    If the raw link-layer isn't enabled.
  *
diff --git a/include/openthread/nat64.h b/include/openthread/nat64.h
index 4589d36..cd1eec2 100644
--- a/include/openthread/nat64.h
+++ b/include/openthread/nat64.h
@@ -436,6 +436,18 @@
 #define OT_IP4_CIDR_STRING_SIZE 20 ///< Length of 000.000.000.000/00 plus a suffix NUL
 
 /**
+ * This function converts a human-readable IPv4 CIDR string into a binary representation.
+ *
+ * @param[in]   aString   A pointer to a NULL-terminated string.
+ * @param[out]  aCidr     A pointer to an IPv4 CIDR.
+ *
+ * @retval OT_ERROR_NONE          Successfully parsed the string.
+ * @retval OT_ERROR_INVALID_ARGS  Failed to parse the string.
+ *
+ */
+otError otIp4CidrFromString(const char *aString, otIp4Cidr *aCidr);
+
+/**
  * Converts the IPv4 CIDR to a string.
  *
  * The string format uses quad-dotted notation of four bytes in the address with the length of prefix (e.g.,
diff --git a/include/openthread/ncp.h b/include/openthread/ncp.h
index fb766ef..4576c7f 100644
--- a/include/openthread/ncp.h
+++ b/include/openthread/ncp.h
@@ -158,7 +158,7 @@
  * @param[in] aAllowPokeDelegate      Delegate function pointer for poke operation.
  *
  */
-void otNcpRegisterPeekPokeDelagates(otNcpDelegateAllowPeekPoke aAllowPeekDelegate,
+void otNcpRegisterPeekPokeDelegates(otNcpDelegateAllowPeekPoke aAllowPeekDelegate,
                                     otNcpDelegateAllowPeekPoke aAllowPokeDelegate);
 
 /**
diff --git a/include/openthread/netdata_publisher.h b/include/openthread/netdata_publisher.h
index 0e68b0e..10346c2 100644
--- a/include/openthread/netdata_publisher.h
+++ b/include/openthread/netdata_publisher.h
@@ -238,6 +238,42 @@
 otError otNetDataPublishExternalRoute(otInstance *aInstance, const otExternalRouteConfig *aConfig);
 
 /**
+ * This function replaces a previously published external route in the Thread Network Data.
+ *
+ * This function requires the feature `OPENTHREAD_CONFIG_BORDER_ROUTER_ENABLE` to be enabled.
+ *
+ * If there is no previously published external route matching @p aPrefix, this function behaves similarly to
+ * `otNetDataPublishExternalRoute()`, i.e., it will start the process of publishing @a aConfig as an external route in
+ * the Thread Network Data.
+ *
+ * If there is a previously published route entry matching @p aPrefix, it will be replaced with the new prefix from
+ * @p aConfig.
+ *
+ * - If the @p aPrefix was already added in the Network Data, the change to the new prefix in @p aConfig is immediately
+ *   reflected in the Network Data. This ensures that route entries in the Network Data are not abruptly removed and
+ *   the transition from aPrefix to the new prefix is smooth.
+ *
+ * - If the old published @p aPrefix was not added in the Network Data, it will be replaced with the new @p aConfig
+ *   prefix but it will not be immediately added. Instead, it will start the process of publishing it in the Network
+ *   Data (monitoring the Network Data to determine when/if to add the prefix, depending on the number of similar
+ *   prefixes present in the Network Data).
+ *
+ * @param[in] aPrefix         The previously published external route prefix to replace.
+ * @param[in] aConfig         The external route config to publish.
+ * @param[in] aRequester      The requester (`kFromUser` or `kFromRoutingManager` module).
+ *
+ * @retval OT_ERROR_NONE          The external route is published successfully.
+ * @retval OT_ERROR_INVALID_ARGS  The @p aConfig is not valid (bad prefix, invalid flag combinations, or not stable).
+ * @retval OT_ERROR_NO_BUFS       Could not allocate an entry for the new request. Publisher supports a limited number
+ *                                of entries (shared between on-mesh prefix and external route) determined by config
+ *                                `OPENTHREAD_CONFIG_NETDATA_PUBLISHER_MAX_PREFIX_ENTRIES`.
+ *
+ */
+otError otNetDataReplacePublishedExternalRoute(otInstance                  *aInstance,
+                                               const otIp6Prefix           *aPrefix,
+                                               const otExternalRouteConfig *aConfig);
+
+/**
  * This function indicates whether or not currently a published prefix entry (on-mesh or external route) is added to
  * the Thread Network Data.
  *
diff --git a/include/openthread/netdiag.h b/include/openthread/netdiag.h
index c4071e2..76e523b 100644
--- a/include/openthread/netdiag.h
+++ b/include/openthread/netdiag.h
@@ -66,25 +66,34 @@
 
 enum
 {
-    OT_NETWORK_DIAGNOSTIC_TLV_EXT_ADDRESS       = 0,  ///< MAC Extended Address TLV
-    OT_NETWORK_DIAGNOSTIC_TLV_SHORT_ADDRESS     = 1,  ///< Address16 TLV
-    OT_NETWORK_DIAGNOSTIC_TLV_MODE              = 2,  ///< Mode TLV
-    OT_NETWORK_DIAGNOSTIC_TLV_TIMEOUT           = 3,  ///< Timeout TLV (the maximum polling time period for SEDs)
-    OT_NETWORK_DIAGNOSTIC_TLV_CONNECTIVITY      = 4,  ///< Connectivity TLV
-    OT_NETWORK_DIAGNOSTIC_TLV_ROUTE             = 5,  ///< Route64 TLV
-    OT_NETWORK_DIAGNOSTIC_TLV_LEADER_DATA       = 6,  ///< Leader Data TLV
-    OT_NETWORK_DIAGNOSTIC_TLV_NETWORK_DATA      = 7,  ///< Network Data TLV
-    OT_NETWORK_DIAGNOSTIC_TLV_IP6_ADDR_LIST     = 8,  ///< IPv6 Address List TLV
-    OT_NETWORK_DIAGNOSTIC_TLV_MAC_COUNTERS      = 9,  ///< MAC Counters TLV
-    OT_NETWORK_DIAGNOSTIC_TLV_BATTERY_LEVEL     = 14, ///< Battery Level TLV
-    OT_NETWORK_DIAGNOSTIC_TLV_SUPPLY_VOLTAGE    = 15, ///< Supply Voltage TLV
-    OT_NETWORK_DIAGNOSTIC_TLV_CHILD_TABLE       = 16, ///< Child Table TLV
-    OT_NETWORK_DIAGNOSTIC_TLV_CHANNEL_PAGES     = 17, ///< Channel Pages TLV
-    OT_NETWORK_DIAGNOSTIC_TLV_TYPE_LIST         = 18, ///< Type List TLV
-    OT_NETWORK_DIAGNOSTIC_TLV_MAX_CHILD_TIMEOUT = 19, ///< Max Child Timeout TLV
-    OT_NETWORK_DIAGNOSTIC_TLV_VERSION           = 24, ///< Version TLV
+    OT_NETWORK_DIAGNOSTIC_TLV_EXT_ADDRESS          = 0,  ///< MAC Extended Address TLV
+    OT_NETWORK_DIAGNOSTIC_TLV_SHORT_ADDRESS        = 1,  ///< Address16 TLV
+    OT_NETWORK_DIAGNOSTIC_TLV_MODE                 = 2,  ///< Mode TLV
+    OT_NETWORK_DIAGNOSTIC_TLV_TIMEOUT              = 3,  ///< Timeout TLV (the maximum polling time period for SEDs)
+    OT_NETWORK_DIAGNOSTIC_TLV_CONNECTIVITY         = 4,  ///< Connectivity TLV
+    OT_NETWORK_DIAGNOSTIC_TLV_ROUTE                = 5,  ///< Route64 TLV
+    OT_NETWORK_DIAGNOSTIC_TLV_LEADER_DATA          = 6,  ///< Leader Data TLV
+    OT_NETWORK_DIAGNOSTIC_TLV_NETWORK_DATA         = 7,  ///< Network Data TLV
+    OT_NETWORK_DIAGNOSTIC_TLV_IP6_ADDR_LIST        = 8,  ///< IPv6 Address List TLV
+    OT_NETWORK_DIAGNOSTIC_TLV_MAC_COUNTERS         = 9,  ///< MAC Counters TLV
+    OT_NETWORK_DIAGNOSTIC_TLV_BATTERY_LEVEL        = 14, ///< Battery Level TLV
+    OT_NETWORK_DIAGNOSTIC_TLV_SUPPLY_VOLTAGE       = 15, ///< Supply Voltage TLV
+    OT_NETWORK_DIAGNOSTIC_TLV_CHILD_TABLE          = 16, ///< Child Table TLV
+    OT_NETWORK_DIAGNOSTIC_TLV_CHANNEL_PAGES        = 17, ///< Channel Pages TLV
+    OT_NETWORK_DIAGNOSTIC_TLV_TYPE_LIST            = 18, ///< Type List TLV
+    OT_NETWORK_DIAGNOSTIC_TLV_MAX_CHILD_TIMEOUT    = 19, ///< Max Child Timeout TLV
+    OT_NETWORK_DIAGNOSTIC_TLV_VERSION              = 24, ///< Version TLV
+    OT_NETWORK_DIAGNOSTIC_TLV_VENDOR_NAME          = 25, ///< Vendor Name TLV
+    OT_NETWORK_DIAGNOSTIC_TLV_VENDOR_MODEL         = 26, ///< Vendor Model TLV
+    OT_NETWORK_DIAGNOSTIC_TLV_VENDOR_SW_VERSION    = 27, ///< Vendor SW Version TLV
+    OT_NETWORK_DIAGNOSTIC_TLV_THREAD_STACK_VERSION = 28, ///< Thread Stack Version TLV
 };
 
+#define OT_NETWORK_DIAGNOSTIC_MAX_VENDOR_NAME_TLV_LENGTH 32          ///< Max length of Vendor Name TLV.
+#define OT_NETWORK_DIAGNOSTIC_MAX_VENDOR_MODEL_TLV_LENGTH 32         ///< Max length of Vendor Model TLV.
+#define OT_NETWORK_DIAGNOSTIC_MAX_VENDOR_SW_VERSION_TLV_LENGTH 16    ///< Max length of Vendor SW Version TLV.
+#define OT_NETWORK_DIAGNOSTIC_MAX_THREAD_STACK_VERSION_TLV_LENGTH 64 ///< Max length of Thread Stack Version TLV.
+
 typedef uint16_t otNetworkDiagIterator; ///< Used to iterate through Network Diagnostic TLV.
 
 /**
@@ -247,6 +256,10 @@
         uint16_t                  mSupplyVoltage;
         uint32_t                  mMaxChildTimeout;
         uint16_t                  mVersion;
+        char                      mVendorName[OT_NETWORK_DIAGNOSTIC_MAX_VENDOR_NAME_TLV_LENGTH + 1];
+        char                      mVendorModel[OT_NETWORK_DIAGNOSTIC_MAX_VENDOR_MODEL_TLV_LENGTH + 1];
+        char                      mVendorSwVersion[OT_NETWORK_DIAGNOSTIC_MAX_VENDOR_SW_VERSION_TLV_LENGTH + 1];
+        char                      mThreadStackVersion[OT_NETWORK_DIAGNOSTIC_MAX_THREAD_STACK_VERSION_TLV_LENGTH + 1];
         struct
         {
             uint8_t mCount;
@@ -274,6 +287,8 @@
 /**
  * This function gets the next Network Diagnostic TLV in the message.
  *
+ * Requires `OPENTHREAD_CONFIG_TMF_NETDIAG_CLIENT_ENABLE`.
+ *
  * @param[in]      aMessage         A pointer to a message.
  * @param[in,out]  aIterator        A pointer to the Network Diagnostic iterator context. To get the first
  *                                  Network Diagnostic TLV it should be set to OT_NETWORK_DIAGNOSTIC_ITERATOR_INIT.
@@ -309,6 +324,8 @@
 /**
  * Send a Network Diagnostic Get request.
  *
+ * Requires `OPENTHREAD_CONFIG_TMF_NETDIAG_CLIENT_ENABLE`.
+ *
  * @param[in]  aInstance         A pointer to an OpenThread instance.
  * @param[in]  aDestination      A pointer to destination address.
  * @param[in]  aTlvTypes         An array of Network Diagnostic TLV types.
@@ -331,6 +348,8 @@
 /**
  * Send a Network Diagnostic Reset request.
  *
+ * Requires `OPENTHREAD_CONFIG_TMF_NETDIAG_CLIENT_ENABLE`.
+ *
  * @param[in]  aInstance      A pointer to an OpenThread instance.
  * @param[in]  aDestination   A pointer to destination address.
  * @param[in]  aTlvTypes      An array of Network Diagnostic TLV types. Currently only Type 9 is allowed.
@@ -346,6 +365,87 @@
                                     uint8_t             aCount);
 
 /**
+ * Get the vendor name string.
+ *
+ * @param[in]  aInstance      A pointer to an OpenThread instance.
+ *
+ * @returns The vendor name string.
+ *
+ */
+const char *otThreadGetVendorName(otInstance *aInstance);
+
+/**
+ * Get the vendor model string.
+ *
+ * @param[in]  aInstance      A pointer to an OpenThread instance.
+ *
+ * @returns The vendor model string.
+ *
+ */
+const char *otThreadGetVendorModel(otInstance *aInstance);
+
+/**
+ * Get the vendor sw version string.
+ *
+ * @param[in]  aInstance      A pointer to an OpenThread instance.
+ *
+ * @returns The vendor sw version string.
+ *
+ */
+const char *otThreadGetVendorSwVersion(otInstance *aInstance);
+
+/**
+ * Set the vendor name string.
+ *
+ * Requires `OPENTHREAD_CONFIG_NET_DIAG_VENDOR_INFO_SET_API_ENABLE`.
+ *
+ * @p aVendorName should be UTF8 with max length of 32 chars (`MAX_VENDOR_NAME_TLV_LENGTH`). Maximum length does not
+ * include the null `\0` character.
+ *
+ * @param[in] aInstance       A pointer to an OpenThread instance.
+ * @param[in] aVendorName     The vendor name string.
+ *
+ * @retval OT_ERROR_NONE          Successfully set the vendor name.
+ * @retval OT_ERROR_INVALID_ARGS  @p aVendorName is not valid (too long or not UTF8).
+ *
+ */
+otError otThreadSetVendorName(otInstance *aInstance, const char *aVendorName);
+
+/**
+ * Set the vendor model string.
+ *
+ * Requires `OPENTHREAD_CONFIG_NET_DIAG_VENDOR_INFO_SET_API_ENABLE`.
+ *
+ * @p aVendorModel should be UTF8 with max length of 32 chars (`MAX_VENDOR_MODEL_TLV_LENGTH`). Maximum length does not
+ * include the null `\0` character.
+ *
+ * @param[in] aInstance       A pointer to an OpenThread instance.
+ * @param[in] aVendorModel    The vendor model string.
+ *
+ * @retval OT_ERROR_NONE          Successfully set the vendor model.
+ * @retval OT_ERROR_INVALID_ARGS  @p aVendorModel is not valid (too long or not UTF8).
+ *
+ */
+otError otThreadSetVendorModel(otInstance *aInstance, const char *aVendorModel);
+
+/**
+ * Set the vendor software version string.
+ *
+ * Requires `OPENTHREAD_CONFIG_NET_DIAG_VENDOR_INFO_SET_API_ENABLE`.
+ *
+ * @p aVendorSwVersion should be UTF8 with max length of 16 chars(`MAX_VENDOR_SW_VERSION_TLV_LENGTH`). Maximum length
+ * does not include the null `\0` character.
+ *
+ * @param[in] aInstance          A pointer to an OpenThread instance.
+ * @param[in] aVendorSwVersion   The vendor software version string.
+ *
+ * @retval OT_ERROR_NONE          Successfully set the vendor software version.
+ * @retval OT_ERROR_INVALID_ARGS  @p aVendorSwVersion is not valid (too long or not UTF8).
+ *
+ */
+otError otThreadSetVendorSwVersion(otInstance *aInstance, const char *aVendorSwVersion);
+
+/**
  * @}
  *
  */
diff --git a/include/openthread/platform/alarm-micro.h b/include/openthread/platform/alarm-micro.h
index e616407..9bbe05d 100644
--- a/include/openthread/platform/alarm-micro.h
+++ b/include/openthread/platform/alarm-micro.h
@@ -53,6 +53,9 @@
 /**
  * Set the alarm to fire at @p aDt microseconds after @p aT0.
  *
+ * For @p aT0, the platform MUST support all values in [0, 2^32-1].
+ * For @p aDt, the platform MUST support all values in [0, 2^31-1].
+ *
  * @param[in]  aInstance  The OpenThread instance structure.
  * @param[in]  aT0        The reference time.
  * @param[in]  aDt        The time delay in microseconds from @p aT0.
@@ -71,6 +74,9 @@
 /**
  * Get the current time.
  *
+ * The current time MUST represent a free-running timer. When maintaining current time, the time value MUST utilize the
+ * entire range [0, 2^32-1] and MUST NOT wrap before 2^32.
+ *
  * @returns  The current time in microseconds.
  *
  */
diff --git a/include/openthread/platform/alarm-milli.h b/include/openthread/platform/alarm-milli.h
index dd72201..48fb08c 100644
--- a/include/openthread/platform/alarm-milli.h
+++ b/include/openthread/platform/alarm-milli.h
@@ -56,6 +56,9 @@
 /**
  * Set the alarm to fire at @p aDt milliseconds after @p aT0.
  *
+ * For @p aT0 the platform MUST support all values in [0, 2^32-1].
+ * For @p aDt, the platform MUST support all values in [0, 2^31-1].
+ *
  * @param[in] aInstance  The OpenThread instance structure.
  * @param[in] aT0        The reference time.
  * @param[in] aDt        The time delay in milliseconds from @p aT0.
@@ -72,6 +75,9 @@
 /**
  * Get the current time.
  *
+ * The current time MUST represent a free-running timer. When maintaining current time, the time value MUST utilize the
+ * entire range [0, 2^32-1] and MUST NOT wrap before 2^32.
+ *
  * @returns The current time in milliseconds.
  */
 uint32_t otPlatAlarmMilliGetNow(void);
diff --git a/include/openthread/platform/crypto.h b/include/openthread/platform/crypto.h
index 69253bb..0bccc38 100644
--- a/include/openthread/platform/crypto.h
+++ b/include/openthread/platform/crypto.h
@@ -60,9 +60,10 @@
  */
 typedef enum
 {
-    OT_CRYPTO_KEY_TYPE_RAW,  ///< Key Type: Raw Data.
-    OT_CRYPTO_KEY_TYPE_AES,  ///< Key Type: AES.
-    OT_CRYPTO_KEY_TYPE_HMAC, ///< Key Type: HMAC.
+    OT_CRYPTO_KEY_TYPE_RAW,   ///< Key Type: Raw Data.
+    OT_CRYPTO_KEY_TYPE_AES,   ///< Key Type: AES.
+    OT_CRYPTO_KEY_TYPE_HMAC,  ///< Key Type: HMAC.
+    OT_CRYPTO_KEY_TYPE_ECDSA, ///< Key Type: ECDSA.
 } otCryptoKeyType;
 
 /**
@@ -74,6 +75,7 @@
     OT_CRYPTO_KEY_ALG_VENDOR,       ///< Key Algorithm: Vendor Defined.
     OT_CRYPTO_KEY_ALG_AES_ECB,      ///< Key Algorithm: AES ECB.
     OT_CRYPTO_KEY_ALG_HMAC_SHA_256, ///< Key Algorithm: HMAC SHA-256.
+    OT_CRYPTO_KEY_ALG_ECDSA,        ///< Key Algorithm: ECDSA.
 } otCryptoKeyAlgorithm;
 
 /**
@@ -82,11 +84,12 @@
  */
 enum
 {
-    OT_CRYPTO_KEY_USAGE_NONE      = 0,      ///< Key Usage: Key Usage is empty.
-    OT_CRYPTO_KEY_USAGE_EXPORT    = 1 << 0, ///< Key Usage: Key can be exported.
-    OT_CRYPTO_KEY_USAGE_ENCRYPT   = 1 << 1, ///< Key Usage: Encryption (vendor defined).
-    OT_CRYPTO_KEY_USAGE_DECRYPT   = 1 << 2, ///< Key Usage: AES ECB.
-    OT_CRYPTO_KEY_USAGE_SIGN_HASH = 1 << 3, ///< Key Usage: HMAC SHA-256.
+    OT_CRYPTO_KEY_USAGE_NONE        = 0,      ///< Key Usage: Key Usage is empty.
+    OT_CRYPTO_KEY_USAGE_EXPORT      = 1 << 0, ///< Key Usage: Key can be exported.
+    OT_CRYPTO_KEY_USAGE_ENCRYPT     = 1 << 1, ///< Key Usage: Encryption (vendor defined).
+    OT_CRYPTO_KEY_USAGE_DECRYPT     = 1 << 2, ///< Key Usage: AES ECB.
+    OT_CRYPTO_KEY_USAGE_SIGN_HASH   = 1 << 3, ///< Key Usage: Sign Hash.
+    OT_CRYPTO_KEY_USAGE_VERIFY_HASH = 1 << 4, ///< Key Usage: Verify Hash.
 };
 
 /**
@@ -651,6 +654,85 @@
                                 const otPlatCryptoEcdsaSignature *aSignature);
 
 /**
+ * Calculate the ECDSA signature for a hashed message using the Key reference passed.
+ *
+ * This method uses the deterministic digital signature generation procedure from RFC 6979.
+ *
+ * @param[in]  aKeyRef            Key Reference to the slot where the key-pair is stored.
+ * @param[in]  aHash              A pointer to a SHA-256 hash structure where the hash value for signature calculation
+ *                                is stored.
+ * @param[out] aSignature         A pointer to an ECDSA signature structure to output the calculated signature.
+ *
+ * @retval OT_ERROR_NONE          The signature was calculated successfully, @p aSignature was updated.
+ * @retval OT_ERROR_PARSE         The key-pair DER format could not be parsed (invalid format).
+ * @retval OT_ERROR_NO_BUFS       Failed to allocate buffer for signature calculation.
+ * @retval OT_ERROR_INVALID_ARGS  The @p aContext is NULL.
+ *
+ * @note This API is only used by OT core when `OPENTHREAD_CONFIG_PLATFORM_KEY_REFERENCES_ENABLE` is enabled.
+ *
+ */
+otError otPlatCryptoEcdsaSignUsingKeyRef(otCryptoKeyRef                aKeyRef,
+                                         const otPlatCryptoSha256Hash *aHash,
+                                         otPlatCryptoEcdsaSignature   *aSignature);
+
+/**
+ * Get the associated public key from the key reference passed.
+ *
+ * The public key is stored differently depending on the crypto backend library being used
+ * (OPENTHREAD_CONFIG_CRYPTO_LIB).
+ *
+ * This API must make sure to return the public key as a byte sequence representation of an
+ * uncompressed curve point (RFC 6605 - sec 4)
+ *
+ * @param[in]  aKeyRef            Key Reference to the slot where the key-pair is stored.
+ * @param[out] aPublicKey         A pointer to an ECDSA public key structure to store the public key.
+ *
+ * @retval OT_ERROR_NONE          Public key was retrieved successfully, and @p aBuffer is updated.
+ * @retval OT_ERROR_PARSE         The key-pair DER format could not be parsed (invalid format).
+ * @retval OT_ERROR_INVALID_ARGS  The @p aContext is NULL.
+ *
+ * @note This API is only used by OT core when `OPENTHREAD_CONFIG_PLATFORM_KEY_REFERENCES_ENABLE` is enabled.
+ *
+ */
+otError otPlatCryptoEcdsaExportPublicKey(otCryptoKeyRef aKeyRef, otPlatCryptoEcdsaPublicKey *aPublicKey);
+
+/**
+ * Generate and import a new ECDSA key-pair at reference passed.
+ *
+ * @param[in]  aKeyRef            Key Reference to the slot where the key-pair is stored.
+ *
+ * @retval OT_ERROR_NONE          A new key-pair was generated successfully.
+ * @retval OT_ERROR_NO_BUFS       Failed to allocate buffer for key generation.
+ * @retval OT_ERROR_NOT_CAPABLE   Feature not supported.
+ * @retval OT_ERROR_FAILED        Failed to generate key-pair.
+ *
+ * @note This API is only used by OT core when `OPENTHREAD_CONFIG_PLATFORM_KEY_REFERENCES_ENABLE` is enabled.
+ *
+ */
+otError otPlatCryptoEcdsaGenerateAndImportKey(otCryptoKeyRef aKeyRef);
+
+/**
+ * Use the keyref to verify the ECDSA signature of a hashed message.
+ *
+ * @param[in]  aKeyRef            Key Reference to the slot where the key-pair is stored.
+ * @param[in]  aHash              A pointer to a SHA-256 hash structure where the hash value for signature verification
+ *                                is stored.
+ * @param[in]  aSignature         A pointer to an ECDSA signature structure where the signature value to be verified is
+ *                                stored.
+ *
+ * @retval OT_ERROR_NONE          The signature was verified successfully.
+ * @retval OT_ERROR_SECURITY      The signature is invalid.
+ * @retval OT_ERROR_INVALID_ARGS  The key or hash is invalid.
+ * @retval OT_ERROR_NO_BUFS       Failed to allocate buffer for signature verification.
+ *
+ * @note This API is only used by OT core when `OPENTHREAD_CONFIG_PLATFORM_KEY_REFERENCES_ENABLE` is enabled.
+ *
+ */
+otError otPlatCryptoEcdsaVerifyUsingKeyRef(otCryptoKeyRef                    aKeyRef,
+                                           const otPlatCryptoSha256Hash     *aHash,
+                                           const otPlatCryptoEcdsaSignature *aSignature);
+
+/**
  * Perform PKCS#5 PBKDF2 using CMAC (AES-CMAC-PRF-128).
  *
  * @param[in]     aPassword          Password to use when generating key.
diff --git a/include/openthread/platform/diag.h b/include/openthread/platform/diag.h
index df889d2..6f65f82 100644
--- a/include/openthread/platform/diag.h
+++ b/include/openthread/platform/diag.h
@@ -261,6 +261,19 @@
 otError otPlatDiagRadioTransmitCarrier(otInstance *aInstance, bool aEnable);
 
 /**
+ * Start/stop the platform layer to transmit stream of characters.
+ *
+ * @param[in]  aInstance The OpenThread instance structure.
+ * @param[in]  aEnable   TRUE to enable or FALSE to disable the platform layer to transmit stream.
+ *
+ * @retval OT_ERROR_NONE             Successfully enabled/disabled.
+ * @retval OT_ERROR_INVALID_STATE    The radio was not in the Receive state.
+ * @retval OT_ERROR_NOT_IMPLEMENTED  This function is not implemented.
+ *
+ */
+otError otPlatDiagRadioTransmitStream(otInstance *aInstance, bool aEnable);
+
+/**
  * Get the power settings for the given channel.
  *
  * @param[in]      aInstance               The OpenThread instance structure.
diff --git a/include/openthread/platform/radio.h b/include/openthread/platform/radio.h
index 89e17be..963f6ee 100644
--- a/include/openthread/platform/radio.h
+++ b/include/openthread/platform/radio.h
@@ -895,7 +895,7 @@
  * @param[in] aScanDuration  The duration, in milliseconds, for the channel to be scanned.
  *
  * @retval OT_ERROR_NONE             Successfully started scanning the channel.
- * @retval OT_ERROR_BUSY             The radio is performing enery scanning.
+ * @retval OT_ERROR_BUSY             The radio is performing energy scanning.
  * @retval OT_ERROR_NOT_IMPLEMENTED  The radio doesn't support energy scanning.
  *
  */
diff --git a/include/openthread/platform/settings.h b/include/openthread/platform/settings.h
index 0b2f035..a9ca05d 100644
--- a/include/openthread/platform/settings.h
+++ b/include/openthread/platform/settings.h
@@ -73,6 +73,7 @@
     OT_SETTINGS_KEY_SRP_SERVER_INFO      = 0x000d, ///< The SRP server info (UDP port).
     OT_SETTINGS_KEY_BR_ULA_PREFIX        = 0x000f, ///< BR ULA prefix.
     OT_SETTINGS_KEY_BR_ON_LINK_PREFIXES  = 0x0010, ///< BR local on-link prefixes.
+    OT_SETTINGS_KEY_BORDER_AGENT_ID      = 0x0011, ///< Unique Border Agent/Router ID.
 
     // Deprecated and reserved key values:
     //
diff --git a/include/openthread/platform/udp.h b/include/openthread/platform/udp.h
index 5b4a21c..b1dae45 100644
--- a/include/openthread/platform/udp.h
+++ b/include/openthread/platform/udp.h
@@ -68,7 +68,7 @@
  *
  * @param[in]   aUdpSocket  A pointer to the UDP socket.
  *
- * @retval  OT_ERROR_NONE   Successfully binded UDP socket by platform.
+ * @retval  OT_ERROR_NONE   Successfully bound UDP socket by platform.
  * @retval  OT_ERROR_FAILED Failed to bind UDP socket.
  *
  */
@@ -107,7 +107,7 @@
  * @param[in]   aMessageInfo    A pointer to the message info associated with @p aMessage.
  *
  * @retval  OT_ERROR_NONE   Successfully sent by platform, and @p aMessage is freed.
- * @retval  OT_ERROR_FAILED Failed to binded UDP socket.
+ * @retval  OT_ERROR_FAILED Failed to bind UDP socket.
  *
  */
 otError otPlatUdpSend(otUdpSocket *aUdpSocket, otMessage *aMessage, const otMessageInfo *aMessageInfo);
diff --git a/include/openthread/srp_server.h b/include/openthread/srp_server.h
index 4dceee2..c3dd36b 100644
--- a/include/openthread/srp_server.h
+++ b/include/openthread/srp_server.h
@@ -327,7 +327,7 @@
  * it stays enabled).
  *
  * @param[in] aInstance   A pointer to an OpenThread instance.
- * @param[in] aEnbaled    A boolean to enable/disable the auto-enable mode.
+ * @param[in] aEnabled    A boolean to enable/disable the auto-enable mode.
  *
  */
 void otSrpServerSetAutoEnableMode(otInstance *aInstance, bool aEnabled);
diff --git a/include/openthread/thread.h b/include/openthread/thread.h
index df663f7..4bde02c 100644
--- a/include/openthread/thread.h
+++ b/include/openthread/thread.h
@@ -90,7 +90,8 @@
 typedef struct
 {
     otExtAddress mExtAddress;           ///< IEEE 802.15.4 Extended Address
-    uint32_t     mAge;                  ///< Time last heard
+    uint32_t     mAge;                  ///< Seconds since last heard
+    uint32_t     mConnectionTime;       ///< Seconds since link establishment (requires `CONFIG_UPTIME_ENABLE`)
     uint16_t     mRloc16;               ///< RLOC16
     uint32_t     mLinkFrameCounter;     ///< Link Frame Counter
     uint32_t     mMleFrameCounter;      ///< MLE Frame Counter
@@ -416,7 +417,7 @@
  * Get the Thread Network Key.
  *
  * @param[in]   aInstance     A pointer to an OpenThread instance.
- * @param[out]  aNetworkKey   A pointer to an `otNetworkkey` to return the Thread Network Key.
+ * @param[out]  aNetworkKey   A pointer to an `otNetworkKey` to return the Thread Network Key.
  *
  * @sa otThreadSetNetworkKey
  *
@@ -922,6 +923,8 @@
 /**
  * This function pointer is called every time an MLE Parent Response message is received.
  *
+ * This is used in `otThreadRegisterParentResponseCallback()`.
+ *
  * @param[in]  aInfo     A pointer to a location on stack holding the stats data.
  * @param[in]  aContext  A pointer to callback client-specific context.
  *
@@ -931,6 +934,8 @@
 /**
  * This function registers a callback to receive MLE Parent Response data.
  *
+ * This function requires `OPENTHREAD_CONFIG_MLE_PARENT_RESPONSE_CALLBACK_API_ENABLE`.
+ *
  * @param[in]  aInstance  A pointer to an OpenThread instance.
  * @param[in]  aCallback  A pointer to a function that is called upon receiving an MLE Parent Response message.
  * @param[in]  aContext   A pointer to callback client-specific context.
@@ -1074,6 +1079,28 @@
  */
 otError otThreadDetachGracefully(otInstance *aInstance, otDetachGracefullyCallback aCallback, void *aContext);
 
+#define OT_DURATION_STRING_SIZE 21 ///< Recommended size for string representation of `uint32_t` duration in seconds.
+
+/**
+ * This function converts an `uint32_t` duration (in seconds) to a human-readable string.
+ *
+ * This function requires `OPENTHREAD_CONFIG_UPTIME_ENABLE` to be enabled.
+ *
+ * The string follows the format "<hh>:<mm>:<ss>" for hours, minutes, seconds (if duration is shorter than one day) or
+ * "<dd>d.<hh>:<mm>:<ss>" (if longer than a day).
+ *
+ * If the resulting string does not fit in @p aBuffer (within its @p aSize characters), the string will be truncated
+ * but the outputted string is always null-terminated.
+ *
+ * This function is intended for use with `mAge` or `mConnectionTime` in `otNeighborInfo` or `otChildInfo` structures.
+ *
+ * @param[in]  aDuration A duration interval in seconds.
+ * @param[out] aBuffer   A pointer to a char array to output the string.
+ * @param[in]  aSize     The size of @p aBuffer (in bytes). Recommended to use `OT_DURATION_STRING_SIZE`.
+ *
+ */
+void otConvertDurationInSecondsToString(uint32_t aDuration, char *aBuffer, uint16_t aSize);
+
 /**
  * @}
  *
diff --git a/include/openthread/thread_ftd.h b/include/openthread/thread_ftd.h
index 0778a0a..f2b0db5 100644
--- a/include/openthread/thread_ftd.h
+++ b/include/openthread/thread_ftd.h
@@ -58,7 +58,8 @@
 {
     otExtAddress mExtAddress;           ///< IEEE 802.15.4 Extended Address
     uint32_t     mTimeout;              ///< Timeout
-    uint32_t     mAge;                  ///< Time last heard
+    uint32_t     mAge;                  ///< Seconds since last heard
+    uint64_t     mConnectionTime;       ///< Seconds since attach (requires `OPENTHREAD_CONFIG_UPTIME_ENABLE`)
     uint16_t     mRloc16;               ///< RLOC16
     uint16_t     mChildId;              ///< Child ID
     uint8_t      mNetworkDataVersion;   ///< Network Data Version
diff --git a/openthread_upstream_version.gni b/openthread_upstream_version.gni
index ecdfe9e..dee6378 100644
--- a/openthread_upstream_version.gni
+++ b/openthread_upstream_version.gni
@@ -1,3 +1,3 @@
 # This file is added to support soft-transition in Fuchsia.
 
-openthread_upstream_version = "4577fb21a74c48cad1500c3dbf1022e1ee72bd65"
+openthread_upstream_version = "323ffd894ba9dfa6362ed0fa5d8eb9a4f9167d01"
diff --git a/script/check-arm-build b/script/check-arm-build
index a2dff82..398dff7 100755
--- a/script/check-arm-build
+++ b/script/check-arm-build
@@ -65,8 +65,8 @@
         "-DOT_MAC_FILTER=ON"
         "-DOT_MESSAGE_USE_HEAP=ON"
         "-DOT_MLR=ON"
-        "-DOT_MTD_NETDIAG=ON"
         "-DOT_NETDATA_PUBLISHER=ON"
+        "-DOT_NETDIAG_CLIENT=ON"
         "-DOT_PING_SENDER=ON"
         "-DOT_SERVICE=ON"
         "-DOT_SLAAC=ON"
diff --git a/script/check-scan-build b/script/check-scan-build
index b04b111..ef723fa 100755
--- a/script/check-scan-build
+++ b/script/check-scan-build
@@ -62,10 +62,10 @@
     "-DOT_LOG_LEVEL_DYNAMIC=ON"
     "-DOT_MAC_FILTER=ON"
     "-DOT_MESH_DIAG=ON"
-    "-DOT_MTD_NETDIAG=ON"
     "-DOT_NAT64_BORDER_ROUTING=ON"
     "-DOT_NAT64_TRANSLATOR=ON"
     "-DOT_NEIGHBOR_DISCOVERY_AGENT=ON"
+    "-DOT_NETDIAG_CLIENT=ON"
     "-DOT_PING_SENDER=ON"
     "-DOT_PLATFORM=external"
     "-DOT_RCP_RESTORATION_MAX_COUNT=2"
@@ -75,6 +75,9 @@
     "-DOT_SNTP_CLIENT=ON"
     "-DOT_SRP_CLIENT=ON"
     "-DOT_SRP_SERVER=ON"
+    "-DOT_VENDOR_NAME=OpenThread"
+    "-DOT_VENDOR_MODEL=Scan-build"
+    "-DOT_VENDOR_SW_VERSION=OT"
 )
 readonly OT_BUILD_OPTIONS
 
diff --git a/script/check-simulation-build-autotools b/script/check-simulation-build-autotools
index 118a293..1e3ef25 100755
--- a/script/check-simulation-build-autotools
+++ b/script/check-simulation-build-autotools
@@ -86,7 +86,7 @@
         "-DOPENTHREAD_CONFIG_SNTP_CLIENT_ENABLE=1"
         "-DOPENTHREAD_CONFIG_SRP_CLIENT_ENABLE=1"
         "-DOPENTHREAD_CONFIG_TMF_NETDATA_SERVICE_ENABLE=1"
-        "-DOPENTHREAD_CONFIG_TMF_NETWORK_DIAG_MTD_ENABLE=1"
+        "-DOPENTHREAD_CONFIG_TMF_NETDIAG_CLIENT_ENABLE=1"
         "-DOPENTHREAD_CONFIG_UDP_FORWARD_ENABLE=1"
         "-DOPENTHREAD_CONFIG_MAC_BEACON_PAYLOAD_PARSING_ENABLE=1"
         "-DOPENTHREAD_CONFIG_MAC_OUTGOING_BEACON_PAYLOAD_ENABLE=1"
diff --git a/script/check-simulation-build-cmake b/script/check-simulation-build-cmake
index 3a0800a..b84677a 100755
--- a/script/check-simulation-build-cmake
+++ b/script/check-simulation-build-cmake
@@ -96,6 +96,11 @@
     # Build with RAM settings
     reset_source
     "$(dirname "$0")"/cmake-build simulation -DOT_SETTINGS_RAM=ON
+
+    # Build with Vendor CLI commands
+    reset_source
+    "$(dirname "$0")"/cmake-build simulation \
+        -DOT_CLI_VENDOR_EXTENSION=../../src/cli/cli_extension_example.cmake
 }
 
 build_toranj()
diff --git a/script/check-size b/script/check-size
index 482dddf..9076626 100755
--- a/script/check-size
+++ b/script/check-size
@@ -142,7 +142,6 @@
         "-DOT_LINK_RAW=ON"
         "-DOT_MAC_FILTER=ON"
         "-DOT_MESSAGE_USE_HEAP=ON"
-        "-DOT_MTD_NETDIAG=ON"
         "-DOT_NETDATA_PUBLISHER=ON"
         "-DOT_PING_SENDER=ON"
         "-DOT_SERVICE=ON"
diff --git a/script/cmake-build b/script/cmake-build
index 3647216..4757286 100755
--- a/script/cmake-build
+++ b/script/cmake-build
@@ -62,48 +62,52 @@
 
 set -euxo pipefail
 
-OT_CMAKE_NINJA_TARGET=${OT_CMAKE_NINJA_TARGET:-}
+OT_CMAKE_NINJA_TARGET=${OT_CMAKE_NINJA_TARGET-}
 
 OT_SRCDIR="$(cd "$(dirname "$0")"/.. && pwd)"
 readonly OT_SRCDIR
 
-OT_PLATFORMS=(simulation posix)
+OT_PLATFORMS=(simulation posix android-ndk)
 readonly OT_PLATFORMS
 
 OT_POSIX_SIM_COMMON_OPTIONS=(
     "-DOT_ANYCAST_LOCATOR=ON"
     "-DOT_BORDER_AGENT=ON"
+    "-DOT_BORDER_AGENT_ID=ON"
     "-DOT_BORDER_ROUTER=ON"
-    "-DOT_COAP=ON"
-    "-DOT_COAP_BLOCK=ON"
-    "-DOT_COAP_OBSERVE=ON"
-    "-DOT_COAPS=ON"
-    "-DOT_COMMISSIONER=ON"
     "-DOT_CHANNEL_MANAGER=ON"
     "-DOT_CHANNEL_MONITOR=ON"
+    "-DOT_CHILD_SUPERVISION=ON"
+    "-DOT_COAP=ON"
+    "-DOT_COAPS=ON"
+    "-DOT_COAP_BLOCK=ON"
+    "-DOT_COAP_OBSERVE=ON"
+    "-DOT_COMMISSIONER=ON"
+    "-DOT_COMPILE_WARNING_AS_ERROR=ON"
+    "-DOT_COVERAGE=ON"
     "-DOT_DATASET_UPDATER=ON"
     "-DOT_DHCP6_CLIENT=ON"
     "-DOT_DHCP6_SERVER=ON"
     "-DOT_DIAGNOSTIC=ON"
+    "-DOT_DNSSD_SERVER=ON"
     "-DOT_DNS_CLIENT=ON"
     "-DOT_ECDSA=ON"
     "-DOT_HISTORY_TRACKER=ON"
     "-DOT_IP6_FRAGM=ON"
     "-DOT_JAM_DETECTION=ON"
     "-DOT_JOINER=ON"
+    "-DOT_LOG_LEVEL_DYNAMIC=ON"
     "-DOT_MAC_FILTER=ON"
-    "-DOT_MTD_NETDIAG=ON"
     "-DOT_NEIGHBOR_DISCOVERY_AGENT=ON"
     "-DOT_NETDATA_PUBLISHER=ON"
+    "-DOT_NETDIAG_CLIENT=ON"
     "-DOT_PING_SENDER=ON"
+    "-DOT_RCP_RESTORATION_MAX_COUNT=2"
     "-DOT_REFERENCE_DEVICE=ON"
     "-DOT_SERVICE=ON"
     "-DOT_SNTP_CLIENT=ON"
     "-DOT_SRP_CLIENT=ON"
-    "-DOT_COVERAGE=ON"
-    "-DOT_LOG_LEVEL_DYNAMIC=ON"
-    "-DOT_COMPILE_WARNING_AS_ERROR=ON"
-    "-DOT_RCP_RESTORATION_MAX_COUNT=2"
+    "-DOT_SRP_SERVER=ON"
     "-DOT_UPTIME=ON"
 )
 readonly OT_POSIX_SIM_COMMON_OPTIONS
@@ -148,11 +152,62 @@
     shift
     local local_options=()
     local options=(
-        "-DOT_PLATFORM=${platform}"
         "-DOT_SLAAC=ON"
     )
 
     case "${platform}" in
+        android-ndk)
+            if [ -z "${NDK-}" ]; then
+                echo "
+The 'NDK' environment variable needs to point to the Android NDK toolchain.
+Please ensure the NDK is downloaded and extracted then try to run this script again
+
+For example:
+    NDK=/opt/android-ndk-r25c ./script/cmake-build-android
+
+You can download the NDK at https://developer.android.com/ndk/downloads
+
+            "
+                exit 1
+            fi
+
+            NDK_CMAKE_TOOLCHAIN_FILE="${NDK?}/build/cmake/android.toolchain.cmake"
+            if [ ! -f "${NDK_CMAKE_TOOLCHAIN_FILE}" ]; then
+                echo "
+Could not fild the Android NDK CMake toolchain file
+- NDK=${NDK}
+- NDK_CMAKE_TOOLCHAIN_FILE=${NDK_CMAKE_TOOLCHAIN_FILE}
+
+            "
+                exit 2
+            fi
+            local_options+=(
+                "-DOT_LOG_OUTPUT=PLATFORM_DEFINED"
+
+                # Add Android NDK flags
+                "-DOT_ANDROID_NDK=1"
+                "-DCMAKE_TOOLCHAIN_FILE=${NDK?}/build/cmake/android.toolchain.cmake"
+
+                # Android API needs to be >= android-24 for `getifsaddrs()`
+                "-DANDROID_PLATFORM=android-24"
+
+                # Store thread settings in the CWD when executing ot-cli or ot-daemon
+                '-DOT_POSIX_SETTINGS_PATH="./thread"'
+            )
+
+            # Rewrite platform to posix
+            platform="posix"
+
+            # Check if OT_DAEMON or OT_APP_CLI flags are needed
+            if [[ ${OT_CMAKE_NINJA_TARGET[*]} =~ "ot-daemon" ]] || [[ ${OT_CMAKE_NINJA_TARGET[*]} =~ "ot-ctl" ]]; then
+                local_options+=("-DOT_DAEMON=ON")
+            elif [[ ${OT_CMAKE_NINJA_TARGET[*]} =~ "ot-cli" ]]; then
+                local_options+=("-DOT_APP_CLI=ON")
+            fi
+
+            options+=("${local_options[@]}")
+            ;;
+
         posix)
             local_options+=(
                 "-DOT_TCP=OFF"
@@ -162,8 +217,11 @@
             options+=("${OT_POSIX_SIM_COMMON_OPTIONS[@]}" "${local_options[@]}")
             ;;
         simulation)
-            local_options=("-DOT_LINK_RAW=ON"
+            local_options+=(
+                "-DOT_LINK_RAW=ON"
+                "-DOT_DNS_DSO=ON"
                 "-DOT_DNS_CLIENT_OVER_TCP=ON"
+                "-DOT_UDP_FORWARD=ON"
             )
             options+=("${OT_POSIX_SIM_COMMON_OPTIONS[@]}" "${local_options[@]}")
             ;;
@@ -172,6 +230,9 @@
             ;;
     esac
 
+    options+=(
+        "-DOT_PLATFORM=${platform}"
+    )
     options+=("$@")
     build "${platform}" "${options[@]}"
 }
diff --git a/tests/scripts/Makefile.am b/script/code-spell
old mode 100644
new mode 100755
similarity index 62%
copy from tests/scripts/Makefile.am
copy to script/code-spell
index d1cd8f1..5e007fc
--- a/tests/scripts/Makefile.am
+++ b/script/code-spell
@@ -1,5 +1,6 @@
+#!/bin/bash
 #
-#  Copyright (c) 2016-2017, The OpenThread Authors.
+#  Copyright (c) 2023, The OpenThread Authors.
 #  All rights reserved.
 #
 #  Redistribution and use in source and binary forms, with or without
@@ -26,26 +27,44 @@
 #  POSSIBILITY OF SUCH DAMAGE.
 #
 
-include $(abs_top_nlbuild_autotools_dir)/automake/pre.am
+#
+# The script to correct or check the code spell of OpenThread.
+#
+# Correct wrong spelling:
+#     script/code-spell
+#
+# Check only:
+#
+#     script/code-spell check
 
-# Always package (e.g. for 'make dist') these subdirectories.
+set -euxo pipefail
 
-DIST_SUBDIRS                            = \
-    thread-cert                           \
-    $(NULL)
+OT_SPELL_CHECK_IGNORE_CONFIG_FILE='.code-spell-ignore'
+readonly OT_SPELL_CHECK_IGNORE_CONFIG_FILE
 
-# Always build (e.g. for 'make all') these subdirectories.
+OT_SPELL_CHECK_DIRS=(
+    'doc'
+    'etc'
+    'examples'
+    'include'
+    'script'
+    'src'
+    'tests'
+    'tools'
+)
+readonly OT_SPELL_CHECK_DIRS
 
-SUBDIRS                                 = \
-    $(NULL)
+main()
+{
+    if [ $# == 0 ]; then
+        codespell "${OT_SPELL_CHECK_DIRS[@]}" -w --ignore-words="${OT_SPELL_CHECK_IGNORE_CONFIG_FILE}"
+    elif [ "$1" == 'check' ]; then
+        codespell "${OT_SPELL_CHECK_DIRS[@]}" --ignore-words="${OT_SPELL_CHECK_IGNORE_CONFIG_FILE}"
+    else
+        echo >&2 "Unsupported option: $1. Supported: check"
+        # 128 for Invalid arguments
+        exit 128
+    fi
+}
 
-if OPENTHREAD_POSIX
-if OPENTHREAD_ENABLE_CLI
-SUBDIRS                                += \
-    thread-cert                           \
-    $(NULL)
-endif
-endif
-
-include $(abs_top_nlbuild_autotools_dir)/automake/post.am
-
+main "$@"
diff --git a/script/make-pretty b/script/make-pretty
index 443ac53..28abdf0 100755
--- a/script/make-pretty
+++ b/script/make-pretty
@@ -121,10 +121,10 @@
     '-DOT_LINK_METRICS_SUBJECT=ON'
     '-DOT_MAC_FILTER=ON'
     '-DOT_MESH_DIAG=ON'
-    '-DOT_MTD_NETDIAG=ON'
     '-DOT_NAT64_BORDER_ROUTING=ON'
     '-DOT_NAT64_TRANSLATOR=ON'
     '-DOT_NETDATA_PUBLISHER=ON'
+    '-DOT_NETDIAG_CLIENT=ON'
     '-DOT_PING_SENDER=ON'
     '-DOT_REFERENCE_DEVICE=ON'
     '-DOT_SERVICE=ON'
diff --git a/script/test b/script/test
index d1e04fe..7fc0c3e 100755
--- a/script/test
+++ b/script/test
@@ -74,7 +74,7 @@
 NAT64_SERVICE="${NAT64_SERVICE:-openthread}"
 readonly NAT64_SERVICE
 
-INTER_OP_BBR="${INTER_OP_BBR:-1}"
+INTER_OP_BBR="${INTER_OP_BBR:-0}"
 readonly INTER_OP_BBR
 
 OT_COREDUMP_DIR="${PWD}/ot-core-dump"
@@ -272,6 +272,7 @@
 do_cert_suite()
 {
     export top_builddir="${OT_BUILDDIR}/openthread-simulation-${THREAD_VERSION}"
+    export top_srcdir="${OT_SRCDIR}"
 
     if [[ ${THREAD_VERSION} != "1.1" ]]; then
         export top_builddir_1_3_bbr="${OT_BUILDDIR}/openthread-simulation-1.3-bbr"
@@ -285,7 +286,8 @@
 
     sudo modprobe ip6table_filter
 
-    python3 tests/scripts/thread-cert/run_cert_suite.py --multiply "${MULTIPLY:-1}" "$@"
+    mkdir -p ot_testing
+    ./tests/scripts/thread-cert/run_cert_suite.py --run-directory ot_testing --multiply "${MULTIPLY:-1}" "$@"
     exit 0
 }
 
@@ -389,7 +391,7 @@
 
 do_pktverify()
 {
-    python3 ./tests/scripts/thread-cert/pktverify/verify.py "$1"
+    ./tests/scripts/thread-cert/pktverify/verify.py "$1"
 }
 
 ot_exec_expect_script()
diff --git a/script/update-makefiles.py b/script/update-makefiles.py
index bbaeca4..3b16e4d 100755
--- a/script/update-makefiles.py
+++ b/script/update-makefiles.py
@@ -28,7 +28,7 @@
 #
 
 # This script updates different make/build files (CMakeLists.txt, BUILD.gn,
-# Andriod.mk, Andriod.bp, auto-make) in OpenThread repo based on the
+# Android.mk, Android.bp, auto-make) in OpenThread repo based on the
 # current files present in `./src/core/` & `./include/openthread/`
 # folders. This script MUST be called from openthread root folder.
 
diff --git a/src/cli/BUILD.gn b/src/cli/BUILD.gn
index 000e74c..bd7f413 100644
--- a/src/cli/BUILD.gn
+++ b/src/cli/BUILD.gn
@@ -41,10 +41,14 @@
   "cli_config.h",
   "cli_dataset.cpp",
   "cli_dataset.hpp",
+  "cli_dns.cpp",
+  "cli_dns.hpp",
   "cli_history.cpp",
   "cli_history.hpp",
   "cli_joiner.cpp",
   "cli_joiner.hpp",
+  "cli_mac_filter.cpp",
+  "cli_mac_filter.hpp",
   "cli_network_data.cpp",
   "cli_network_data.hpp",
   "cli_output.cpp",
diff --git a/src/cli/CMakeLists.txt b/src/cli/CMakeLists.txt
index 32c9d7b..a94d3e0 100644
--- a/src/cli/CMakeLists.txt
+++ b/src/cli/CMakeLists.txt
@@ -38,8 +38,10 @@
     cli_coap_secure.cpp
     cli_commissioner.cpp
     cli_dataset.cpp
+    cli_dns.cpp
     cli_history.cpp
     cli_joiner.cpp
+    cli_mac_filter.cpp
     cli_network_data.cpp
     cli_output.cpp
     cli_srp_client.cpp
@@ -48,6 +50,13 @@
     cli_udp.cpp
 )
 
+set(OT_CLI_VENDOR_EXTENSION "" CACHE STRING "Path to CMake file to define and link cli vendor extension")
+if(OT_CLI_VENDOR_EXTENSION)
+    set(OT_CLI_VENDOR_TARGET "" CACHE STRING "Name of vendor extension CMake target to link with cli app")
+    include(${OT_CLI_VENDOR_EXTENSION})
+    target_compile_definitions(ot-config INTERFACE "OPENTHREAD_CONFIG_CLI_VENDOR_COMMANDS_ENABLE=1")
+endif()
+
 if(OT_FTD)
     include(ftd.cmake)
 endif()
diff --git a/src/cli/Makefile.am b/src/cli/Makefile.am
index 43d1b47..f17f544 100644
--- a/src/cli/Makefile.am
+++ b/src/cli/Makefile.am
@@ -65,23 +65,23 @@
 #
 #     Thus, the existing(previous) way *THIS* library was compiled is
 #     exactly the FTD path. Meaning the "cli" library always sees
-#     the FTD varients of various headers/classes.
+#     the FTD variants of various headers/classes.
 #
 #     The same is true of the "ncp" library.
 #
-# HOWEVER there are two varients of the CLI application, CLI-MTD
-# and CLI-FTD (and likewise, two varients of the ncp application)
+# HOWEVER there are two variants of the CLI application, CLI-MTD
+# and CLI-FTD (and likewise, two variants of the ncp application)
 # These applications link against two different OpenThread libraries.
 #
 # Which flavor, you get depends upon which library: "mtd" or "ftd" is linked.
 #
 # Which on the surface appear to link fine against the MTD/FTD library.
 #
-# In this description/example we focus on the  "nework_data_leader"
-# header file. The FTD varient has many private variables, functions
+# In this description/example we focus on the  "network_data_leader"
+# header file. The FTD variant has many private variables, functions
 # and other things of "FTD" (ie: full) implementation items.
 #
-# In contrast the MTD is generaly stubbed out with stub-functions
+# In contrast the MTD is generally stubbed out with stub-functions
 # inlined in the header that return "error not implemented" or similar.
 #
 # Thus it works... here ... With this file and this example.
@@ -92,20 +92,20 @@
 #    Is this true always? Is this robust?
 #    Or is there a hidden "got-ya" that will snag the next person?
 #
-# This also fails static analisys, checks.
+# This also fails static analysis, checks.
 #    Application - with MTD vrs FTD class.
 #    Library #1  (cli-lib) with FTD selected.
 #    Library #2  (openthread) with two different class flavors.
 #
-# The static analisys tools will say: "NOPE" different classes!
+# The static analysis tools will say: "NOPE" different classes!
 # Perhaps this will change if/when nothing is implemented in the 'mtd-header'
 #
 # Additionally, tools that perform "whole program optimization" will
-# throw errors becuase the data structures differ greatly.
+# throw errors because the data structures differ greatly.
 #
 # Hence, CLI library (and NCP) must exist in two flavors.
 #
-# Unless and until these libraries do not "accidently" suck in
+# Unless and until these libraries do not "accidentally" suck in
 # a "flavored" header file somewhere.
 
 lib_LIBRARIES                       = $(NULL)
@@ -158,8 +158,10 @@
     cli_coap_secure.cpp               \
     cli_commissioner.cpp              \
     cli_dataset.cpp                   \
+    cli_dns.cpp                       \
     cli_history.cpp                   \
     cli_joiner.cpp                    \
+    cli_mac_filter.cpp                \
     cli_network_data.cpp              \
     cli_output.cpp                    \
     cli_srp_client.cpp                \
@@ -189,8 +191,10 @@
     cli_commissioner.hpp              \
     cli_config.h                      \
     cli_dataset.hpp                   \
+    cli_dns.hpp                       \
     cli_history.hpp                   \
     cli_joiner.hpp                    \
+    cli_mac_filter.hpp                \
     cli_network_data.hpp              \
     cli_output.hpp                    \
     cli_srp_client.hpp                \
diff --git a/src/cli/README.md b/src/cli/README.md
index 6d8369f..15a6384 100644
--- a/src/cli/README.md
+++ b/src/cli/README.md
@@ -56,6 +56,7 @@
 - [fem](#fem)
 - [history](README_HISTORY.md)
 - [ifconfig](#ifconfig)
+- [instanceid](#instanceid)
 - [ipaddr](#ipaddr)
 - [ipmaddr](#ipmaddr)
 - [joiner](README_JOINER.md)
@@ -121,6 +122,7 @@
 - [udp](README_UDP.md)
 - [unsecureport](#unsecureport-add-port)
 - [uptime](#uptime)
+- [vendor](#vendor-name)
 - [version](#version)
 
 ## OpenThread Command Details
@@ -254,7 +256,7 @@
 
 ### bbr enable
 
-Enable Backbone Router Service for Thread 1.2 FTD. `SRV_DATA.ntf` would be triggerred for attached device if there is no Backbone Router Service in Thread Network Data.
+Enable Backbone Router Service for Thread 1.2 FTD. `SRV_DATA.ntf` would be triggered for attached device if there is no Backbone Router Service in Thread Network Data.
 
 `OPENTHREAD_CONFIG_BACKBONE_ROUTER_ENABLE` is required.
 
@@ -265,7 +267,7 @@
 
 ### bbr disable
 
-Disable Backbone Router Service for Thread 1.2 FTD. `SRV_DATA.ntf` would be triggerred if Backbone Router is Primary state. o `OPENTHREAD_CONFIG_BACKBONE_ROUTER_ENABLE` is required.
+Disable Backbone Router Service for Thread 1.2 FTD. `SRV_DATA.ntf` would be triggered if Backbone Router is Primary state. o `OPENTHREAD_CONFIG_BACKBONE_ROUTER_ENABLE` is required.
 
 ```bash
 > bbr disable
@@ -274,7 +276,7 @@
 
 ### bbr register
 
-Register Backbone Router Service for Thread 1.2 FTD. `SRV_DATA.ntf` would be triggerred for attached device.
+Register Backbone Router Service for Thread 1.2 FTD. `SRV_DATA.ntf` would be triggered for attached device.
 
 `OPENTHREAD_CONFIG_BACKBONE_ROUTER_ENABLE` is required.
 
@@ -1113,7 +1115,20 @@
 
 Get the default query config used by DNS client.
 
-The config includes the server IPv6 address and port, response timeout in msec (wait time to rx response), maximum tx attempts before reporting failure, boolean flag to indicate whether the server can resolve the query recursively or not.
+The config includes
+
+- Server IPv6 address and port
+- Response timeout in msec (wait time to rx response)
+- Maximum tx attempts before reporting failure
+- Boolean flag to indicate whether the server can resolve the query recursively or not.
+- Service resolution mode which specifies which records to query. Possible options are:
+  - `srv` : Query for SRV record only.
+  - `txt` : Query for TXT record only.
+  - `srv_txt` : Query for both SRV and TXT records in the same message.
+  - `srv_txt_sep`: Query in parallel for SRV and TXT using separate messages.
+  - `srv_txt_opt`: Query for TXT/SRV together first, if it fails then query separately.
+- Whether to allow/disallow NAT64 address translation during address resolution (requires `OPENTHREAD_CONFIG_DNS_CLIENT_NAT64_ENABLE`)
+- Transport protocol UDP or TCP (requires `OPENTHREAD_CONFIG_DNS_CLIENT_OVER_TCP_ENABLE`)
 
 ```bash
 > dns config
@@ -1121,19 +1136,30 @@
 ResponseTimeout: 5000 ms
 MaxTxAttempts: 2
 RecursionDesired: no
+ServiceMode: srv_txt_opt
+Nat64Mode: allow
 TransportProtocol: udp
 Done
 >
 ```
 
-### dns config \[DNS server IP\] \[DNS server port\] \[response timeout (ms)\] \[max tx attempts\] \[recursion desired (boolean)\] \[transport protocol\]
+### dns config \[DNS server IP\] \[DNS server port\] \[response timeout (ms)\] \[max tx attempts\] \[recursion desired (boolean)\] \[service mode]
 
 Set the default query config.
 
+Service mode specifies which records to query. Possible options are:
+
+- `def` : Use default option.
+- `srv` : Query for SRV record only.
+- `txt` : Query for TXT record only.
+- `srv_txt` : Query for both SRV and TXT records in the same message.
+- `srv_txt_sep`: Query in parallel for SRV and TXT using separate messages.
+- `srv_txt_opt`: Query for TXT/SRV together first, if it fails then query separately.
+
 To set protocol effectively to tcp `OPENTHREAD_CONFIG_DNS_CLIENT_OVER_TCP_ENABLE` is required.
 
 ```bash
-> dns config fd00::1 1234 5000 2 0 tcp
+> dns config fd00::1 1234 5000 2 0 srv_txt_sep tcp
 Done
 
 > dns config
@@ -1141,6 +1167,8 @@
 ResponseTimeout: 5000 ms
 MaxTxAttempts: 2
 RecursionDesired: no
+ServiceMode: srv_txt_sep
+Nat64Mode: allow
 TransportProtocol: tcp
 Done
 ```
@@ -1242,6 +1270,14 @@
 
 > Note: The DNS server IP can be an IPv4 address, which will be synthesized to an IPv6 address using the preferred NAT64 prefix from the network data. The command will return `InvalidState` when the DNS server IP is an IPv4 address but the preferred NAT64 prefix is unavailable.
 
+### dns servicehost \<service-instance-label\> \<service-name\> \[DNS server IP\] \[DNS server port\] \[response timeout (ms)\] \[max tx attempts\] \[recursion desired (boolean)\]
+
+Send a service instance resolution DNS query for a given service instance with a potential follow-up address resolution for the host name discovered for the service instance (if the server/resolver does not provide AAAA/A records for the host name in the response to SRV query).
+
+Service instance label is provided first, followed by the service name (note that service instance label can contain dot '.' character).
+
+The parameters after `service-name` are optional. Any unspecified (or zero) value for these optional parameters is replaced by the value from the current default config (`dns config`).
+
 ### dns compression \[enable|disable\]
 
 Enable/Disable the "DNS name compression" mode.
@@ -1290,7 +1326,7 @@
 
 ### dua iid
 
-Get the Interface Identifier mannually specified for Thread Domain Unicast Address on Thread 1.2 device.
+Get the Interface Identifier manually specified for Thread Domain Unicast Address on Thread 1.2 device.
 
 ```bash
 > dua iid
@@ -1300,7 +1336,7 @@
 
 ### dua iid \<iid\>
 
-Set the Interface Identifier mannually specified for Thread Domain Unicast Address on Thread 1.2 device.
+Set the Interface Identifier manually specified for Thread Domain Unicast Address on Thread 1.2 device.
 
 ```bash
 > dua iid 0004000300020001
@@ -1309,7 +1345,7 @@
 
 ### dua iid clear
 
-Clear the Interface Identifier mannually specified for Thread Domain Unicast Address on Thread 1.2 device.
+Clear the Interface Identifier manually specified for Thread Domain Unicast Address on Thread 1.2 device.
 
 ```bash
 > dua iid clear
@@ -1456,6 +1492,16 @@
 Done
 ```
 
+### instanceid
+
+Show OpenThread instance identifier.
+
+```bash
+> instanceid
+468697314
+Done
+```
+
 ### ipaddr
 
 List all IPv6 addresses assigned to the Thread interface.
@@ -1468,7 +1514,7 @@
 Done
 ```
 
-Use `-v` to get more verbose informations about the address.
+Use `-v` to get more verbose information about the address.
 
 ```bash
 > ipaddr -v
@@ -1644,7 +1690,7 @@
 
 ### keysequence guardtime \<guardtime\>
 
-Set Thread Key Switch Guard Time (in hours) 0 means Thread Key Switch imediately if key index match
+Set Thread Key Switch Guard Time (in hours) 0 means Thread Key Switch immediately if key index match
 
 ```bash
 > keysequence guardtime 0
@@ -2167,14 +2213,14 @@
 Possible results for prefix manager are (`OPENTHREAD_CONFIG_NAT64_BORDER_ROUTING_ENABLE` is required):
 
 - `Disabled`: NAT64 prefix manager is disabled.
-- `NotRunning`: NAT64 prefix manager is enabled, but is not running, probably bacause the routing manager is disabled.
+- `NotRunning`: NAT64 prefix manager is enabled, but is not running, probably because the routing manager is disabled.
 - `Idle`: NAT64 prefix manager is enabled and is running, but is not publishing a NAT64 prefix. Usually when there is another border router publishing a NAT64 prefix with higher priority.
 - `Active`: NAT64 prefix manager is enabled, running and publishing a NAT64 prefix.
 
 Possible results for NAT64 translator are (`OPENTHREAD_CONFIG_NAT64_TRANSLATOR_ENABLE` is required):
 
 - `Disabled`: NAT64 translator is disabled.
-- `NotRunning`: NAT64 translator is enabled, but is not translating packets, probably bacause it is not configued with a NAT64 prefix or a CIDR for NAT64.
+- `NotRunning`: NAT64 translator is enabled, but is not translating packets, probably because it is not configured with a NAT64 prefix or a CIDR for NAT64.
 - `Active`: NAT64 translator is enabled and is translating packets.
 
 `OPENTHREAD_CONFIG_NAT64_TRANSLATOR_ENABLE` or `OPENTHREAD_CONFIG_NAT64_BORDER_ROUTING_ENABLE` are required.
@@ -2276,6 +2322,43 @@
 Done
 ```
 
+### neighbor conntime
+
+Print connection time and age of neighbors.
+
+The table provides the following info per neighbor:
+
+- RLOC16
+- Extended MAC address
+- Age (seconds since last heard from neighbor)
+- Connection time (seconds since link establishment with neighbor)
+
+Duration intervals are formatted as `<hh>:<mm>:<ss>` for hours, minutes, and seconds if the duration is less than one day. If the duration is longer than one day, the format is `<dd>d.<hh>:<mm>:<ss>`.
+
+```bash
+> neighbor conntime
+| RLOC16 | Extended MAC     | Last Heard (Age) | Connection Time  |
++--------+------------------+------------------+------------------+
+| 0x8401 | 1a28be396a14a318 |         00:00:13 |         00:07:59 |
+| 0x5c00 | 723ebf0d9eba3264 |         00:00:03 |         00:11:27 |
+| 0xe800 | ce53628a1e3f5b3c |         00:00:02 |         00:00:15 |
+Done
+```
+
+### neighbor conntime list
+
+Print connection time and age of neighbors.
+
+This command is similar to `neighbor conntime`, but it displays the information in a list format. The age and connection time are both displayed in seconds.
+
+```bash
+> neighbor conntime list
+0x8401 1a28be396a14a318 age:63 conn-time:644
+0x5c00 723ebf0d9eba3264 age:23 conn-time:852
+0xe800 ce53628a1e3f5b3c age:23 conn-time:180
+Done
+```
+
 ### netstat
 
 List all UDP sockets.
@@ -2613,7 +2696,7 @@
 
 ### prefix
 
-Get the prefix list in the local Network Data. Note: For the Thread 1.2 border router with backbone capability, the local Domain Prefix would be listed as well (with flag `D`), with preceeding `-` if backbone functionality is disabled.
+Get the prefix list in the local Network Data. Note: For the Thread 1.2 border router with backbone capability, the local Domain Prefix would be listed as well (with flag `D`), with preceding `-` if backbone functionality is disabled.
 
 ```bash
 > prefix
@@ -3334,6 +3417,57 @@
 >
 ```
 
+### vendor name
+
+Get the vendor name.
+
+```bash
+> vendor name
+nest
+Done
+```
+
+Set the vendor name (requires `OPENTHREAD_CONFIG_NET_DIAG_VENDOR_INFO_SET_API_ENABLE`).
+
+```bash
+> vendor name nest
+Done
+```
+
+### vendor model
+
+Get the vendor model.
+
+```bash
+> vendor model
+Hub Max
+Done
+```
+
+Set the vendor model (requires `OPENTHREAD_CONFIG_NET_DIAG_VENDOR_INFO_SET_API_ENABLE`).
+
+```bash
+> vendor model Hub\ Max
+Done
+```
+
+### vendor swversion
+
+Get the vendor SW version.
+
+```bash
+> vendor swversion
+Marble3.5.1
+Done
+```
+
+Set the vendor SW version (requires `OPENTHREAD_CONFIG_NET_DIAG_VENDOR_INFO_SET_API_ENABLE`).
+
+```bash
+> vendor swversion Marble3.5.1
+Done
+```
+
 ### version
 
 Print the build version information.
@@ -3417,7 +3551,7 @@
 0f6127e33af6b402
 RssIn List:
 0f6127e33af6b403 : rss -95 (lqi 1)
-Default rss : -50 (lqi 3)
+Default rss: -50 (lqi 3)
 Done
 ```
 
@@ -3462,7 +3596,7 @@
 
 ### macfilter addr add \<extaddr\> \[rss\]
 
-Add an IEEE 802.15.4 Extended Address to the address filter, and fixed the received singal strength for the messages from the address if rss is specified.
+Add an IEEE 802.15.4 Extended Address to the address filter, and fixed the received signal strength for the messages from the address if rss is specified.
 
 ```bash
 > macfilter addr add 0f6127e33af6b403 -95
diff --git a/src/cli/README_DATASET.md b/src/cli/README_DATASET.md
index df1b274..a1882af 100644
--- a/src/cli/README_DATASET.md
+++ b/src/cli/README_DATASET.md
@@ -392,7 +392,7 @@
 Get network name.
 
 ```bash
-> datset networkname
+> dataset networkname
 OpenThread
 Done
 ```
diff --git a/src/cli/README_HISTORY.md b/src/cli/README_HISTORY.md
index 268cf7d..3fa681a 100644
--- a/src/cli/README_HISTORY.md
+++ b/src/cli/README_HISTORY.md
@@ -475,19 +475,19 @@
 ```bash
 > history rx list 4
 00:00:13.368
-    type:UDP len:50 cheksum:0xbd26 sec:no prio:net rss:-20 from:0x4800 radio:15.4
+    type:UDP len:50 checksum:0xbd26 sec:no prio:net rss:-20 from:0x4800 radio:15.4
     src:[fe80:0:0:0:d03d:d3e7:cc5e:7cd7]:19788
     dst:[ff02:0:0:0:0:0:0:1]:19788
 00:00:14.991
-    type:HopOpts len:44 cheksum:0x0000 sec:yes prio:norm rss:-20 from:0x4800 radio:15.4
+    type:HopOpts len:44 checksum:0x0000 sec:yes prio:norm rss:-20 from:0x4800 radio:15.4
     src:[fdde:ad00:beef:0:0:ff:fe00:4800]:0
     dst:[ff03:0:0:0:0:0:0:2]:0
 00:00:15.030
-    type:UDP len:12 cheksum:0x3f7d sec:yes prio:net rss:-20 from:0x4800 radio:15.4
+    type:UDP len:12 checksum:0x3f7d sec:yes prio:net rss:-20 from:0x4800 radio:15.4
     src:[fdde:ad00:beef:0:0:ff:fe00:4800]:61631
     dst:[fdde:ad00:beef:0:0:ff:fe00:4801]:61631
 00:00:15.032
-    type:ICMP6(EchoReqst) len:16 cheksum:0x942c sec:yes prio:norm rss:-20 from:0x4800 radio:15.4
+    type:ICMP6(EchoReqst) len:16 checksum:0x942c sec:yes prio:norm rss:-20 from:0x4800 radio:15.4
     src:[fdde:ad00:beef:0:ac09:a16b:3204:dc09]:0
     dst:[fdde:ad00:beef:0:dc0e:d6b3:f180:b75b]:0
 Done
@@ -577,23 +577,23 @@
 > history rxtx list 5
 
 00:00:02.100
-    type:UDP len:50 cheksum:0xd843 sec:no prio:net rss:-20 from:0x0800 radio:15.4
+    type:UDP len:50 checksum:0xd843 sec:no prio:net rss:-20 from:0x0800 radio:15.4
     src:[fe80:0:0:0:54d9:5153:ffc6:df26]:19788
     dst:[ff02:0:0:0:0:0:0:1]:19788
 00:00:15.331
-    type:HopOpts len:44 cheksum:0x0000 sec:yes prio:norm rss:-20 from:0x0800 radio:15.4
+    type:HopOpts len:44 checksum:0x0000 sec:yes prio:norm rss:-20 from:0x0800 radio:15.4
     src:[fdde:ad00:beef:0:0:ff:fe00:800]:0
     dst:[ff03:0:0:0:0:0:0:2]:0
 00:00:15.354
-    type:UDP len:12 cheksum:0x6c6b sec:yes prio:net rss:-20 from:0x0800 radio:15.4
+    type:UDP len:12 checksum:0x6c6b sec:yes prio:net rss:-20 from:0x0800 radio:15.4
     src:[fdde:ad00:beef:0:0:ff:fe00:800]:61631
     dst:[fdde:ad00:beef:0:0:ff:fe00:801]:61631
 00:00:15.356
-    type:ICMP6(EchoReqst) len:16 cheksum:0xc6a2 sec:yes prio:norm rss:-20 from:0x0800 radio:15.4
+    type:ICMP6(EchoReqst) len:16 checksum:0xc6a2 sec:yes prio:norm rss:-20 from:0x0800 radio:15.4
     src:[fdde:ad00:beef:0:efe8:4910:cf95:dee9]:0
     dst:[fdde:ad00:beef:0:af4c:3644:882a:3698]:0
 00:00:15.356
-    type:ICMP6(EchoReply) len:16 cheksum:0xc5a2 sec:yes prio:norm tx-success:yes to:0x0800 radio:15.4
+    type:ICMP6(EchoReply) len:16 checksum:0xc5a2 sec:yes prio:norm tx-success:yes to:0x0800 radio:15.4
     src:[fdde:ad00:beef:0:af4c:3644:882a:3698]:0
     dst:[fdde:ad00:beef:0:efe8:4910:cf95:dee9]:0
 ```
@@ -633,19 +633,19 @@
 ```bash
 history tx list
 00:00:23.957
-    type:ICMP6(EchoReply) len:16 cheksum:0x932c sec:yes prio:norm tx-success:yes to:0x4800 radio:15.4
+    type:ICMP6(EchoReply) len:16 checksum:0x932c sec:yes prio:norm tx-success:yes to:0x4800 radio:15.4
     src:[fdde:ad00:beef:0:dc0e:d6b3:f180:b75b]:0
     dst:[fdde:ad00:beef:0:ac09:a16b:3204:dc09]:0
 00:00:23.959
-    type:UDP len:50 cheksum:0xce87 sec:yes prio:net tx-success:yes to:0x4800 radio:15.4
+    type:UDP len:50 checksum:0xce87 sec:yes prio:net tx-success:yes to:0x4800 radio:15.4
     src:[fdde:ad00:beef:0:0:ff:fe00:4801]:61631
     dst:[fdde:ad00:beef:0:0:ff:fe00:4800]:61631
 00:00:44.658
-    type:UDP len:64 cheksum:0xf7ba sec:no prio:net tx-success:yes to:0x4800 radio:15.4
+    type:UDP len:64 checksum:0xf7ba sec:no prio:net tx-success:yes to:0x4800 radio:15.4
     src:[fe80:0:0:0:a4a5:bbac:a8e:bd07]:19788
     dst:[fe80:0:0:0:d03d:d3e7:cc5e:7cd7]:19788
 00:00:45.415
-    type:UDP len:44 cheksum:0x26d4 sec:no prio:net tx-success:yes to:0xffff radio:15.4
+    type:UDP len:44 checksum:0x26d4 sec:no prio:net tx-success:yes to:0xffff radio:15.4
     src:[fe80:0:0:0:a4a5:bbac:a8e:bd07]:19788
     dst:[ff02:0:0:0:0:0:0:2]:19788
 Done
diff --git a/src/cli/README_NETDATA.md b/src/cli/README_NETDATA.md
index b5c8ff0..618a92b 100644
--- a/src/cli/README_NETDATA.md
+++ b/src/cli/README_NETDATA.md
@@ -47,7 +47,7 @@
    Done
    ```
 
-4. Observe IPv6 addresses assigned to the Thread inteface.
+4. Observe IPv6 addresses assigned to the Thread interface.
 
    ```bash
    > ipaddr
@@ -269,6 +269,21 @@
 Done
 ```
 
+### publish replace \<old prefix\> \<prefix\> [sn][prf]
+
+Replace a previously published external route entry.
+
+If there is no previously published external route matching old prefix, this command behaves similarly to `netdata publish route`. If there is a previously published route entry, it will be replaced with the new prefix. In particular, if the old prefix was already added in the Network Data, the change to the new prefix is immediately reflected in the Network Data (i.e., old prefix is removed and the new prefix is added in the same Network Data registration request to leader). This ensures that route entries in the Network Data are not abruptly removed.
+
+- s: Stable flag
+- n: NAT64 flag
+- prf: Preference, which may be: 'high', 'med', or 'low'.
+
+```bash
+> netdata publish replace ::/0 fd00:1234:5678::/64 s high
+Done
+```
+
 ### register
 
 Usage: `netdata register`
diff --git a/src/cli/cli.cpp b/src/cli/cli.cpp
index d0b2eae..af5bc6a 100644
--- a/src/cli/cli.cpp
+++ b/src/cli/cli.cpp
@@ -100,8 +100,6 @@
 Interpreter::Interpreter(Instance *aInstance, otCliOutputCallback aCallback, void *aContext)
     : OutputImplementer(aCallback, aContext)
     , Output(aInstance, *this)
-    , mUserCommands(nullptr)
-    , mUserCommandsLength(0)
     , mCommandIsPending(false)
     , mTimer(*aInstance, HandleTimer, this)
 #if OPENTHREAD_FTD || OPENTHREAD_MTD
@@ -111,6 +109,12 @@
     , mDataset(aInstance, *this)
     , mNetworkData(aInstance, *this)
     , mUdp(aInstance, *this)
+#if OPENTHREAD_CONFIG_MAC_FILTER_ENABLE
+    , mMacFilter(aInstance, *this)
+#endif
+#if OPENTHREAD_CLI_DNS_ENABLE
+    , mDns(aInstance, *this)
+#endif
 #if OPENTHREAD_CONFIG_BORDER_ROUTING_ENABLE
     , mBr(aInstance, *this)
 #endif
@@ -149,6 +153,7 @@
 #if (OPENTHREAD_FTD || OPENTHREAD_MTD) && OPENTHREAD_CONFIG_CLI_REGISTER_IP6_RECV_CALLBACK
     otIp6SetReceiveCallback(GetInstancePtr(), &Interpreter::HandleIp6Receive, this);
 #endif
+    memset(&mUserCommands, 0, sizeof(mUserCommands));
 
     OutputPrompt();
 }
@@ -295,26 +300,42 @@
 {
     otError error = OT_ERROR_INVALID_COMMAND;
 
-    for (uint8_t i = 0; i < mUserCommandsLength; i++)
+    for (const UserCommandsEntry &entry : mUserCommands)
     {
-        if (aArgs[0] == mUserCommands[i].mName)
+        for (uint8_t i = 0; i < entry.mLength; i++)
         {
-            char *args[kMaxArgs];
+            if (aArgs[0] == entry.mCommands[i].mName)
+            {
+                char *args[kMaxArgs];
 
-            Arg::CopyArgsToStringArray(aArgs, args);
-            error = mUserCommands[i].mCommand(mUserCommandsContext, Arg::GetArgsLength(aArgs) - 1, args + 1);
-            break;
+                Arg::CopyArgsToStringArray(aArgs, args);
+                error = entry.mCommands[i].mCommand(entry.mContext, Arg::GetArgsLength(aArgs) - 1, args + 1);
+                break;
+            }
         }
     }
 
     return error;
 }
 
-void Interpreter::SetUserCommands(const otCliCommand *aCommands, uint8_t aLength, void *aContext)
+otError Interpreter::SetUserCommands(const otCliCommand *aCommands, uint8_t aLength, void *aContext)
 {
-    mUserCommands        = aCommands;
-    mUserCommandsLength  = aLength;
-    mUserCommandsContext = aContext;
+    otError error = OT_ERROR_FAILED;
+
+    for (UserCommandsEntry &entry : mUserCommands)
+    {
+        if (entry.mCommands == nullptr)
+        {
+            entry.mCommands = aCommands;
+            entry.mLength   = aLength;
+            entry.mContext  = aContext;
+
+            error = OT_ERROR_NONE;
+            break;
+        }
+    }
+
+    return error;
 }
 
 #if OPENTHREAD_FTD || OPENTHREAD_MTD
@@ -530,11 +551,50 @@
 
         OutputLine("%s", Stringify(otBorderAgentGetState(GetInstancePtr()), kStateStrings));
     }
+#if OPENTHREAD_CONFIG_BORDER_AGENT_ID_ENABLE
+    /**
+     * @cli ba id (get,set)
+     * @code
+     * ba id
+     * cb6da1e0c0448aaec39fa90f3d58f45c
+     * Done
+     * @endcode
+     * @code
+     * ba id 00112233445566778899aabbccddeeff
+     * Done
+     * @endcode
+     * @cparam ba id [@ca{border-agent-id}]
+     * Use the optional `border-agent-id` argument to set the Border Agent ID.
+     * @par
+     * Gets or sets the 16 bytes Border Router ID which can uniquely identifies the device among multiple BRs.
+     * @sa otBorderAgentGetId
+     * @sa otBorderAgentSetId
+     */
+    else if (aArgs[0] == "id")
+    {
+        otBorderAgentId id;
+
+        if (aArgs[1].IsEmpty())
+        {
+            SuccessOrExit(error = otBorderAgentGetId(GetInstancePtr(), &id));
+            OutputBytesLine(id.mId);
+        }
+        else
+        {
+            uint16_t idLength = sizeof(id);
+
+            SuccessOrExit(error = aArgs[1].ParseAsHexString(idLength, id.mId));
+            VerifyOrExit(idLength == sizeof(id), error = OT_ERROR_INVALID_ARGS);
+            error = otBorderAgentSetId(GetInstancePtr(), &id);
+        }
+    }
+#endif // OPENTHREAD_CONFIG_BORDER_AGENT_ID_ENABLE
     else
     {
-        error = OT_ERROR_INVALID_COMMAND;
+        ExitNow(error = OT_ERROR_INVALID_COMMAND);
     }
 
+exit:
     return error;
 }
 #endif // OPENTHREAD_CONFIG_BORDER_AGENT_ENABLE
@@ -683,9 +743,8 @@
         otNat64InitAddressMappingIterator(GetInstancePtr(), &iterator);
         while (otNat64GetNextAddressMapping(GetInstancePtr(), &iterator, &mapping) == OT_ERROR_NONE)
         {
-            char               ip4AddressString[OT_IP4_ADDRESS_STRING_SIZE];
-            char               ip6AddressString[OT_IP6_PREFIX_STRING_SIZE];
-            Uint64StringBuffer u64StringBuffer;
+            char ip4AddressString[OT_IP4_ADDRESS_STRING_SIZE];
+            char ip6AddressString[OT_IP6_PREFIX_STRING_SIZE];
 
             otIp6AddressToString(&mapping.mIp6, ip6AddressString, sizeof(ip6AddressString));
             otIp4AddressToString(&mapping.mIp4, ip4AddressString, sizeof(ip4AddressString));
@@ -695,36 +754,19 @@
             OutputFormat("| %40s ", ip6AddressString);
             OutputFormat("| %16s ", ip4AddressString);
             OutputFormat("| %5lus ", ToUlong(mapping.mRemainingTimeMs / 1000));
-            OutputFormat("| %8s ", Uint64ToString(mapping.mCounters.mTotal.m4To6Packets, u64StringBuffer));
-            OutputFormat("| %12s ", Uint64ToString(mapping.mCounters.mTotal.m4To6Bytes, u64StringBuffer));
-            OutputFormat("| %8s ", Uint64ToString(mapping.mCounters.mTotal.m6To4Packets, u64StringBuffer));
-            OutputFormat("| %12s ", Uint64ToString(mapping.mCounters.mTotal.m6To4Bytes, u64StringBuffer));
-
-            OutputLine("|");
+            OutputNat64Counters(mapping.mCounters.mTotal);
 
             OutputFormat("| %16s ", "");
             OutputFormat("| %68s ", "TCP");
-            OutputFormat("| %8s ", Uint64ToString(mapping.mCounters.mTcp.m4To6Packets, u64StringBuffer));
-            OutputFormat("| %12s ", Uint64ToString(mapping.mCounters.mTcp.m4To6Bytes, u64StringBuffer));
-            OutputFormat("| %8s ", Uint64ToString(mapping.mCounters.mTcp.m6To4Packets, u64StringBuffer));
-            OutputFormat("| %12s ", Uint64ToString(mapping.mCounters.mTcp.m6To4Bytes, u64StringBuffer));
-            OutputLine("|");
+            OutputNat64Counters(mapping.mCounters.mTcp);
 
             OutputFormat("| %16s ", "");
             OutputFormat("| %68s ", "UDP");
-            OutputFormat("| %8s ", Uint64ToString(mapping.mCounters.mUdp.m4To6Packets, u64StringBuffer));
-            OutputFormat("| %12s ", Uint64ToString(mapping.mCounters.mUdp.m4To6Bytes, u64StringBuffer));
-            OutputFormat("| %8s ", Uint64ToString(mapping.mCounters.mUdp.m6To4Packets, u64StringBuffer));
-            OutputFormat("| %12s ", Uint64ToString(mapping.mCounters.mUdp.m6To4Bytes, u64StringBuffer));
-            OutputLine("|");
+            OutputNat64Counters(mapping.mCounters.mUdp);
 
             OutputFormat("| %16s ", "");
             OutputFormat("| %68s ", "ICMP");
-            OutputFormat("| %8s ", Uint64ToString(mapping.mCounters.mIcmp.m4To6Packets, u64StringBuffer));
-            OutputFormat("| %12s ", Uint64ToString(mapping.mCounters.mIcmp.m4To6Bytes, u64StringBuffer));
-            OutputFormat("| %8s ", Uint64ToString(mapping.mCounters.mIcmp.m6To4Packets, u64StringBuffer));
-            OutputFormat("| %12s ", Uint64ToString(mapping.mCounters.mIcmp.m6To4Bytes, u64StringBuffer));
-            OutputLine("|");
+            OutputNat64Counters(mapping.mCounters.mIcmp);
         }
     }
     /**
@@ -797,28 +839,16 @@
         otNat64GetErrorCounters(GetInstancePtr(), &errorCounters);
 
         OutputFormat("| %13s ", "Total");
-        OutputFormat("| %8s ", Uint64ToString(counters.mTotal.m4To6Packets, u64StringBuffer));
-        OutputFormat("| %12s ", Uint64ToString(counters.mTotal.m4To6Bytes, u64StringBuffer));
-        OutputFormat("| %8s ", Uint64ToString(counters.mTotal.m6To4Packets, u64StringBuffer));
-        OutputLine("| %12s |", Uint64ToString(counters.mTotal.m6To4Bytes, u64StringBuffer));
+        OutputNat64Counters(counters.mTotal);
 
         OutputFormat("| %13s ", "TCP");
-        OutputFormat("| %8s ", Uint64ToString(counters.mTcp.m4To6Packets, u64StringBuffer));
-        OutputFormat("| %12s ", Uint64ToString(counters.mTcp.m4To6Bytes, u64StringBuffer));
-        OutputFormat("| %8s ", Uint64ToString(counters.mTcp.m6To4Packets, u64StringBuffer));
-        OutputLine("| %12s |", Uint64ToString(counters.mTcp.m6To4Bytes, u64StringBuffer));
+        OutputNat64Counters(counters.mTcp);
 
         OutputFormat("| %13s ", "UDP");
-        OutputFormat("| %8s ", Uint64ToString(counters.mUdp.m4To6Packets, u64StringBuffer));
-        OutputFormat("| %12s ", Uint64ToString(counters.mUdp.m4To6Bytes, u64StringBuffer));
-        OutputFormat("| %8s ", Uint64ToString(counters.mUdp.m6To4Packets, u64StringBuffer));
-        OutputLine("| %12s |", Uint64ToString(counters.mUdp.m6To4Bytes, u64StringBuffer));
+        OutputNat64Counters(counters.mUdp);
 
         OutputFormat("| %13s ", "ICMP");
-        OutputFormat("| %8s ", Uint64ToString(counters.mIcmp.m4To6Packets, u64StringBuffer));
-        OutputFormat("| %12s ", Uint64ToString(counters.mIcmp.m4To6Bytes, u64StringBuffer));
-        OutputFormat("| %8s ", Uint64ToString(counters.mIcmp.m6To4Packets, u64StringBuffer));
-        OutputLine("| %12s |", Uint64ToString(counters.mIcmp.m6To4Bytes, u64StringBuffer));
+        OutputNat64Counters(counters.mIcmp);
 
         OutputTableHeader(kNat64CounterTableErrorSubHeader, kNat64CounterTableErrorSubHeaderColumns);
         for (uint8_t i = 0; i < OT_NAT64_DROP_REASON_COUNT; i++)
@@ -837,6 +867,19 @@
 exit:
     return error;
 }
+
+#if OPENTHREAD_CONFIG_NAT64_TRANSLATOR_ENABLE
+void Interpreter::OutputNat64Counters(const otNat64Counters &aCounters)
+{
+    Uint64StringBuffer u64StringBuffer;
+
+    OutputFormat("| %8s ", Uint64ToString(aCounters.m4To6Packets, u64StringBuffer));
+    OutputFormat("| %12s ", Uint64ToString(aCounters.m4To6Bytes, u64StringBuffer));
+    OutputFormat("| %8s ", Uint64ToString(aCounters.m6To4Packets, u64StringBuffer));
+    OutputLine("| %12s |", Uint64ToString(aCounters.m6To4Bytes, u64StringBuffer));
+}
+#endif
+
 #endif // OPENTHREAD_CONFIG_NAT64_TRANSLATOR_ENABLE || OPENTHREAD_CONFIG_NAT64_BORDER_ROUTING_ENABLE
 
 #if (OPENTHREAD_CONFIG_THREAD_VERSION >= OT_THREAD_VERSION_1_2)
@@ -1240,24 +1283,18 @@
 }
 #endif // OPENTHREAD_FTD && OPENTHREAD_CONFIG_BACKBONE_ROUTER_ENABLE
 
+/**
+ * @cli domainname
+ * @code
+ * domainname
+ * Thread
+ * Done
+ * @endcode
+ * @par api_copy
+ * #otThreadGetDomainName
+ */
 template <> otError Interpreter::Process<Cmd("domainname")>(Arg aArgs[])
 {
-    otError error = OT_ERROR_NONE;
-
-    /**
-     * @cli domainname
-     * @code
-     * domainname
-     * Thread
-     * Done
-     * @endcode
-     * @par api_copy
-     * #otThreadGetDomainName
-     */
-    if (aArgs[0].IsEmpty())
-    {
-        OutputLine("%s", otThreadGetDomainName(GetInstancePtr()));
-    }
     /**
      * @cli domainname (set)
      * @code
@@ -1269,13 +1306,7 @@
      * @par api_copy
      * #otThreadSetDomainName
      */
-    else
-    {
-        SuccessOrExit(error = otThreadSetDomainName(GetInstancePtr(), aArgs[0].GetCString()));
-    }
-
-exit:
-    return error;
+    return ProcessGetSet(aArgs, otThreadGetDomainName, otThreadSetDomainName);
 }
 
 #if OPENTHREAD_CONFIG_DUA_ENABLE
@@ -2908,541 +2939,10 @@
     return error;
 }
 
-template <> otError Interpreter::Process<Cmd("dns")>(Arg aArgs[])
-{
-    OT_UNUSED_VARIABLE(aArgs);
-
-    otError error = OT_ERROR_NONE;
-#if OPENTHREAD_CONFIG_DNS_CLIENT_ENABLE
-    otDnsQueryConfig  queryConfig;
-    otDnsQueryConfig *config = &queryConfig;
+#if OPENTHREAD_CLI_DNS_ENABLE
+template <> otError Interpreter::Process<Cmd("dns")>(Arg aArgs[]) { return mDns.Process(aArgs); }
 #endif
 
-    if (aArgs[0].IsEmpty())
-    {
-        error = OT_ERROR_INVALID_ARGS;
-    }
-    /**
-     * @cli dns compression
-     * @code
-     * dns compression
-     * Enabled
-     * @endcode
-     * @cparam dns compression [@ca{enable|disable}]
-     * @par api_copy
-     * #otDnsIsNameCompressionEnabled
-     * @par
-     * By default DNS name compression is enabled. When disabled,
-     * DNS names are appended as full and never compressed. This
-     * is applicable to OpenThread's DNS and SRP client/server
-     * modules."
-     * 'OPENTHREAD_CONFIG_REFERENCE_DEVICE_ENABLE' is required.
-     */
-#if OPENTHREAD_CONFIG_REFERENCE_DEVICE_ENABLE
-    else if (aArgs[0] == "compression")
-    {
-        /**
-         * @cli dns compression (enable,disable)
-         * @code
-         * dns compression enable
-         * Enabled
-         * @endcode
-         * @code
-         * dns compression disable
-         * Done
-         * dns compression
-         * Disabled
-         * Done
-         * @endcode
-         * @cparam dns compression [@ca{enable|disable}]
-         * @par
-         * Set the "DNS name compression" mode.
-         * @par
-         * By default DNS name compression is enabled. When disabled,
-         * DNS names are appended as full and never compressed. This
-         * is applicable to OpenThread's DNS and SRP client/server
-         * modules."
-         * 'OPENTHREAD_CONFIG_REFERENCE_DEVICE_ENABLE' is required.
-         * @sa otDnsSetNameCompressionEnabled
-         */
-        if (aArgs[1].IsEmpty())
-        {
-            OutputEnabledDisabledStatus(otDnsIsNameCompressionEnabled());
-        }
-        else
-        {
-            bool enable;
-
-            SuccessOrExit(error = ParseEnableOrDisable(aArgs[1], enable));
-            otDnsSetNameCompressionEnabled(enable);
-        }
-    }
-#endif // OPENTHREAD_CONFIG_REFERENCE_DEVICE_ENABLE
-#if OPENTHREAD_CONFIG_DNS_CLIENT_ENABLE
-
-    else if (aArgs[0] == "config")
-    {
-        /**
-         * @cli dns config
-         * @code
-         * dns config
-         * Server: [fd00:0:0:0:0:0:0:1]:1234
-         * ResponseTimeout: 5000 ms
-         * MaxTxAttempts: 2
-         * RecursionDesired: no
-         * Done
-         * @endcode
-         * @par api_copy
-         * #otDnsClientGetDefaultConfig
-         * @par
-         * The config includes the server IPv6 address and port, response
-         * timeout in msec (wait time to rx response), maximum tx attempts
-         * before reporting failure, boolean flag to indicate whether the server
-         * can resolve the query recursively or not.
-         * 'OPENTHREAD_CONFIG_DNS_CLIENT_ENABLE' is required.
-         */
-        if (aArgs[1].IsEmpty())
-        {
-            const otDnsQueryConfig *defaultConfig = otDnsClientGetDefaultConfig(GetInstancePtr());
-
-            OutputFormat("Server: ");
-            OutputSockAddrLine(defaultConfig->mServerSockAddr);
-            OutputLine("ResponseTimeout: %lu ms", ToUlong(defaultConfig->mResponseTimeout));
-            OutputLine("MaxTxAttempts: %u", defaultConfig->mMaxTxAttempts);
-            OutputLine("RecursionDesired: %s",
-                       (defaultConfig->mRecursionFlag == OT_DNS_FLAG_RECURSION_DESIRED) ? "yes" : "no");
-#if OPENTHREAD_CONFIG_DNS_CLIENT_OVER_TCP_ENABLE
-            OutputLine("TransportProtocol: %s",
-                       (defaultConfig->mTransportProto == OT_DNS_TRANSPORT_UDP) ? "udp" : "tcp");
-#endif
-        }
-        /**
-         * @cli dns config (set)
-         * @code
-         * dns config fd00::1 1234 5000 2 0
-         * Done
-         * @endcode
-         * @code
-         * dns config
-         * Server: [fd00:0:0:0:0:0:0:1]:1234
-         * ResponseTimeout: 5000 ms
-         * MaxTxAttempts: 2
-         * RecursionDesired: no
-         * Done
-         * @endcode
-         * @code
-         * dns config fd00::2
-         * Done
-         * @endcode
-         * @code
-         * dns config
-         * Server: [fd00:0:0:0:0:0:0:2]:53
-         * ResponseTimeout: 3000 ms
-         * MaxTxAttempts: 3
-         * RecursionDesired: yes
-         * Done
-         * @endcode
-         * @par api_copy
-         * #otDnsClientSetDefaultConfig
-         * @cparam dns config [@ca{dns-server-IP}] [@ca{dns-server-port}] <!--
-         * -->                [@ca{response-timeout-ms}] [@ca{max-tx-attempts}] <!--
-         * -->                [@ca{recursion-desired-boolean}]
-         * @par
-         * We can leave some of the fields as unspecified (or use value zero). The
-         * unspecified fields are replaced by the corresponding OT config option
-         * definitions OPENTHREAD_CONFIG_DNS_CLIENT_DEFAULT to form the default
-         * query config.
-         * 'OPENTHREAD_CONFIG_DNS_CLIENT_ENABLE' is required.
-         */
-        else
-        {
-            SuccessOrExit(error = GetDnsConfig(aArgs + 1, config));
-            otDnsClientSetDefaultConfig(GetInstancePtr(), config);
-        }
-    }
-    /**
-     * @cli dns resolve
-     * @code
-     * dns resolve ipv6.google.com
-     * DNS response for ipv6.google.com - 2a00:1450:401b:801:0:0:0:200e TTL: 300
-     * @endcode
-     * @code
-     * dns resolve example.com 8.8.8.8
-     * Synthesized IPv6 DNS server address: fdde:ad00:beef:2:0:0:808:808
-     * DNS response for example.com. - fd4c:9574:3720:2:0:0:5db8:d822 TTL:20456
-     * Done
-     * @endcode
-     * @cparam dns resolve @ca{hostname} [@ca{dns-server-IP}] <!--
-     * -->                 [@ca{dns-server-port}] [@ca{response-timeout-ms}] <!--
-     * -->                 [@ca{max-tx-attempts}] [@ca{recursion-desired-boolean}]
-     * @par api_copy
-     * #otDnsClientResolveAddress
-     * @par
-     * Send DNS Query to obtain IPv6 address for given hostname.
-     * @par
-     * The parameters after hostname are optional. Any unspecified (or zero) value
-     * for these optional parameters is replaced by the value from the current default
-     * config (dns config).
-     * @par
-     * The DNS server IP can be an IPv4 address, which will be synthesized to an
-     * IPv6 address using the preferred NAT64 prefix from the network data.
-     * @par
-     * Note: The command will return InvalidState when the DNS server IP is an IPv4
-     * address but the preferred NAT64 prefix is unavailable.
-     * 'OPENTHREAD_CONFIG_DNS_CLIENT_ENABLE' is required.
-     */
-    else if (aArgs[0] == "resolve")
-    {
-        VerifyOrExit(!aArgs[1].IsEmpty(), error = OT_ERROR_INVALID_ARGS);
-        SuccessOrExit(error = GetDnsConfig(aArgs + 2, config));
-        SuccessOrExit(error = otDnsClientResolveAddress(GetInstancePtr(), aArgs[1].GetCString(),
-                                                        &Interpreter::HandleDnsAddressResponse, this, config));
-        error = OT_ERROR_PENDING;
-    }
-#if OPENTHREAD_CONFIG_DNS_CLIENT_NAT64_ENABLE
-    else if (aArgs[0] == "resolve4")
-    {
-        VerifyOrExit(!aArgs[1].IsEmpty(), error = OT_ERROR_INVALID_ARGS);
-        SuccessOrExit(error = GetDnsConfig(aArgs + 2, config));
-        SuccessOrExit(error = otDnsClientResolveIp4Address(GetInstancePtr(), aArgs[1].GetCString(),
-                                                           &Interpreter::HandleDnsAddressResponse, this, config));
-        error = OT_ERROR_PENDING;
-    }
-#endif
-#if OPENTHREAD_CONFIG_DNS_CLIENT_SERVICE_DISCOVERY_ENABLE
-    /**
-     * @cli dns browse
-     * @code
-     * dns browse _service._udp.example.com
-     * DNS browse response for _service._udp.example.com.
-     * inst1
-     *     Port:1234, Priority:1, Weight:2, TTL:7200
-     *     Host:host.example.com.
-     *     HostAddress:fd00:0:0:0:0:0:0:abcd TTL:7200
-     *     TXT:[a=6531, b=6c12] TTL:7300
-     * instance2
-     *     Port:1234, Priority:1, Weight:2, TTL:7200
-     *     Host:host.example.com.
-     *     HostAddress:fd00:0:0:0:0:0:0:abcd TTL:7200
-     *     TXT:[a=1234] TTL:7300
-     * Done
-     * @endcode
-     * @code
-     * dns browse _airplay._tcp.default.service.arpa
-     * DNS browse response for _airplay._tcp.default.service.arpa.
-     * Mac mini
-     *     Port:7000, Priority:0, Weight:0, TTL:10
-     *     Host:Mac-mini.default.service.arpa.
-     *     HostAddress:fd97:739d:386a:1:1c2e:d83c:fcbe:9cf4 TTL:10
-     * Done
-     * @endcode
-     * @cparam dns browse @ca{service-name} [@ca{dns-server-IP}] [@ca{dns-server-port}] <!--
-     * -->                [@ca{response-timeout-ms}] [@ca{max-tx-attempts}] <!--
-     * -->                [@ca{recursion-desired-boolean}]
-     * @sa otDnsClientBrowse
-     * @par
-     * Send a browse (service instance enumeration) DNS query to get the list of services for
-     * given service-name
-     * @par
-     * The parameters after `service-name` are optional. Any unspecified (or zero) value
-     * for these optional parameters is replaced by the value from the current default
-     * config (`dns config`).
-     * @par
-     * Note: The DNS server IP can be an IPv4 address, which will be synthesized to an IPv6
-     * address using the preferred NAT64 prefix from the network data. The command will return
-     * `InvalidState` when the DNS server IP is an IPv4 address but the preferred NAT64 prefix
-     * is unavailable. When testing DNS-SD discovery proxy, the zone is not `local` and
-     * instead should be `default.service.arpa`.
-     * 'OPENTHREAD_CONFIG_DNS_CLIENT_SERVICE_DISCOVERY_ENABLE' is required.
-     */
-    else if (aArgs[0] == "browse")
-    {
-        VerifyOrExit(!aArgs[1].IsEmpty(), error = OT_ERROR_INVALID_ARGS);
-        SuccessOrExit(error = GetDnsConfig(aArgs + 2, config));
-        SuccessOrExit(error = otDnsClientBrowse(GetInstancePtr(), aArgs[1].GetCString(),
-                                                &Interpreter::HandleDnsBrowseResponse, this, config));
-        error = OT_ERROR_PENDING;
-    }
-    /**
-     * @cli dns service
-     * @cparam dns service @ca{service-instance-label} @ca{service-name} <!--
-     * -->                 [@ca{DNS-server-IP}] [@ca{DNS-server-port}] <!--
-     * -->                 [@ca{response-timeout-ms}] [@ca{max-tx-attempts}] <!--
-     * -->                 [@ca{recursion-desired-boolean}]
-     * @par api_copy
-     * #otDnsClientResolveService
-     * @par
-     * Send a service instance resolution DNS query for a given service instance.
-     * Service instance label is provided first, followed by the service name
-     * (note that service instance label can contain dot '.' character).
-     * @par
-     * The parameters after `service-name` are optional. Any unspecified (or zero)
-     * value for these optional parameters is replaced by the value from the
-     * current default config (`dns config`).
-     * @par
-     * Note: The DNS server IP can be an IPv4 address, which will be synthesized
-     * to an IPv6 address using the preferred NAT64 prefix from the network data.
-     * The command will return `InvalidState` when the DNS * server IP is an IPv4
-     * address but the preferred NAT64 prefix is unavailable.
-     * 'OPENTHREAD_CONFIG_DNS_CLIENT_SERVICE_DISCOVERY_ENABLE' is required.
-     */
-    else if (aArgs[0] == "service")
-    {
-        VerifyOrExit(!aArgs[2].IsEmpty(), error = OT_ERROR_INVALID_ARGS);
-        SuccessOrExit(error = GetDnsConfig(aArgs + 3, config));
-        SuccessOrExit(error = otDnsClientResolveService(GetInstancePtr(), aArgs[1].GetCString(), aArgs[2].GetCString(),
-                                                        &Interpreter::HandleDnsServiceResponse, this, config));
-        error = OT_ERROR_PENDING;
-    }
-#endif // OPENTHREAD_CONFIG_DNS_CLIENT_SERVICE_DISCOVERY_ENABLE
-#endif // OPENTHREAD_CONFIG_DNS_CLIENT_ENABLE
-#if OPENTHREAD_CONFIG_DNSSD_SERVER_ENABLE
-    else if (aArgs[0] == "server")
-    {
-        if (aArgs[1].IsEmpty())
-        {
-            error = OT_ERROR_INVALID_ARGS;
-        }
-#if OPENTHREAD_CONFIG_DNS_UPSTREAM_QUERY_ENABLE
-        else if (aArgs[1] == "upstream")
-        {
-            /**
-             * @cli dns server upstream
-             * @code
-             * dns server upstream
-             * Enabled
-             * Done
-             * @endcode
-             * @par api_copy
-             * #otDnssdUpstreamQueryIsEnabled
-             */
-            if (aArgs[2].IsEmpty())
-            {
-                OutputEnabledDisabledStatus(otDnssdUpstreamQueryIsEnabled(GetInstancePtr()));
-            }
-            /**
-             * @cli dns server upstream {enable|disable}
-             * @code
-             * dns server upstream enable
-             * Done
-             * @endcode
-             * @cparam dns server upstream @ca{enable|disable}
-             * @par api_copy
-             * #otDnssdUpstreamQuerySetEnabled
-             */
-            else
-            {
-                bool enable;
-
-                SuccessOrExit(error = ParseEnableOrDisable(aArgs[2], enable));
-                otDnssdUpstreamQuerySetEnabled(GetInstancePtr(), enable);
-            }
-        }
-#endif // OPENTHREAD_CONFIG_DNS_UPSTREAM_QUERY_ENABLE
-        else
-        {
-            ExitNow(error = OT_ERROR_INVALID_COMMAND);
-        }
-    }
-#endif // OPENTHREAD_CONFIG_DNSSD_SERVER_ENABLE
-    else
-    {
-        ExitNow(error = OT_ERROR_INVALID_COMMAND);
-    }
-
-exit:
-    return error;
-}
-
-#if OPENTHREAD_CONFIG_DNS_CLIENT_ENABLE
-
-otError Interpreter::GetDnsConfig(Arg aArgs[], otDnsQueryConfig *&aConfig)
-{
-    // This method gets the optional DNS config from `aArgs[]`.
-    // The format: `[server IP address] [server port] [timeout]
-    // [max tx attempt] [recursion desired]`.
-
-    otError error = OT_ERROR_NONE;
-    bool    recursionDesired;
-    bool    nat64SynthesizedAddress;
-
-    memset(aConfig, 0, sizeof(otDnsQueryConfig));
-
-    VerifyOrExit(!aArgs[0].IsEmpty(), aConfig = nullptr);
-
-    SuccessOrExit(error = Interpreter::ParseToIp6Address(GetInstancePtr(), aArgs[0], aConfig->mServerSockAddr.mAddress,
-                                                         nat64SynthesizedAddress));
-    if (nat64SynthesizedAddress)
-    {
-        OutputFormat("Synthesized IPv6 DNS server address: ");
-        OutputIp6AddressLine(aConfig->mServerSockAddr.mAddress);
-    }
-
-    VerifyOrExit(!aArgs[1].IsEmpty());
-    SuccessOrExit(error = aArgs[1].ParseAsUint16(aConfig->mServerSockAddr.mPort));
-
-    VerifyOrExit(!aArgs[2].IsEmpty());
-    SuccessOrExit(error = aArgs[2].ParseAsUint32(aConfig->mResponseTimeout));
-
-    VerifyOrExit(!aArgs[3].IsEmpty());
-    SuccessOrExit(error = aArgs[3].ParseAsUint8(aConfig->mMaxTxAttempts));
-
-    VerifyOrExit(!aArgs[4].IsEmpty());
-    SuccessOrExit(error = aArgs[4].ParseAsBool(recursionDesired));
-    aConfig->mRecursionFlag = recursionDesired ? OT_DNS_FLAG_RECURSION_DESIRED : OT_DNS_FLAG_NO_RECURSION;
-
-    VerifyOrExit(!aArgs[5].IsEmpty());
-    if (aArgs[5] == "tcp")
-    {
-        aConfig->mTransportProto = OT_DNS_TRANSPORT_TCP;
-    }
-    else if (aArgs[5] == "udp")
-    {
-        aConfig->mTransportProto = OT_DNS_TRANSPORT_UDP;
-    }
-    else
-    {
-        error = OT_ERROR_INVALID_ARGS;
-    }
-exit:
-    return error;
-}
-
-void Interpreter::HandleDnsAddressResponse(otError aError, const otDnsAddressResponse *aResponse, void *aContext)
-{
-    static_cast<Interpreter *>(aContext)->HandleDnsAddressResponse(aError, aResponse);
-}
-
-void Interpreter::HandleDnsAddressResponse(otError aError, const otDnsAddressResponse *aResponse)
-{
-    char         hostName[OT_DNS_MAX_NAME_SIZE];
-    otIp6Address address;
-    uint32_t     ttl;
-
-    IgnoreError(otDnsAddressResponseGetHostName(aResponse, hostName, sizeof(hostName)));
-
-    OutputFormat("DNS response for %s - ", hostName);
-
-    if (aError == OT_ERROR_NONE)
-    {
-        uint16_t index = 0;
-
-        while (otDnsAddressResponseGetAddress(aResponse, index, &address, &ttl) == OT_ERROR_NONE)
-        {
-            OutputIp6Address(address);
-            OutputFormat(" TTL:%lu ", ToUlong(ttl));
-            index++;
-        }
-    }
-
-    OutputNewLine();
-    OutputResult(aError);
-}
-
-#if OPENTHREAD_CONFIG_DNS_CLIENT_SERVICE_DISCOVERY_ENABLE
-
-void Interpreter::OutputDnsServiceInfo(uint8_t aIndentSize, const otDnsServiceInfo &aServiceInfo)
-{
-    OutputLine(aIndentSize, "Port:%d, Priority:%d, Weight:%d, TTL:%lu", aServiceInfo.mPort, aServiceInfo.mPriority,
-               aServiceInfo.mWeight, ToUlong(aServiceInfo.mTtl));
-    OutputLine(aIndentSize, "Host:%s", aServiceInfo.mHostNameBuffer);
-    OutputFormat(aIndentSize, "HostAddress:");
-    OutputIp6Address(aServiceInfo.mHostAddress);
-    OutputLine(" TTL:%lu", ToUlong(aServiceInfo.mHostAddressTtl));
-    OutputFormat(aIndentSize, "TXT:");
-
-    if (!aServiceInfo.mTxtDataTruncated)
-    {
-        OutputDnsTxtData(aServiceInfo.mTxtData, aServiceInfo.mTxtDataSize);
-    }
-    else
-    {
-        OutputFormat("[");
-        OutputBytes(aServiceInfo.mTxtData, aServiceInfo.mTxtDataSize);
-        OutputFormat("...]");
-    }
-
-    OutputLine(" TTL:%lu", ToUlong(aServiceInfo.mTxtDataTtl));
-}
-
-void Interpreter::HandleDnsBrowseResponse(otError aError, const otDnsBrowseResponse *aResponse, void *aContext)
-{
-    static_cast<Interpreter *>(aContext)->HandleDnsBrowseResponse(aError, aResponse);
-}
-
-void Interpreter::HandleDnsBrowseResponse(otError aError, const otDnsBrowseResponse *aResponse)
-{
-    char             name[OT_DNS_MAX_NAME_SIZE];
-    char             label[OT_DNS_MAX_LABEL_SIZE];
-    uint8_t          txtBuffer[kMaxTxtDataSize];
-    otDnsServiceInfo serviceInfo;
-
-    IgnoreError(otDnsBrowseResponseGetServiceName(aResponse, name, sizeof(name)));
-
-    OutputLine("DNS browse response for %s", name);
-
-    if (aError == OT_ERROR_NONE)
-    {
-        uint16_t index = 0;
-
-        while (otDnsBrowseResponseGetServiceInstance(aResponse, index, label, sizeof(label)) == OT_ERROR_NONE)
-        {
-            OutputLine("%s", label);
-            index++;
-
-            serviceInfo.mHostNameBuffer     = name;
-            serviceInfo.mHostNameBufferSize = sizeof(name);
-            serviceInfo.mTxtData            = txtBuffer;
-            serviceInfo.mTxtDataSize        = sizeof(txtBuffer);
-
-            if (otDnsBrowseResponseGetServiceInfo(aResponse, label, &serviceInfo) == OT_ERROR_NONE)
-            {
-                OutputDnsServiceInfo(kIndentSize, serviceInfo);
-            }
-
-            OutputNewLine();
-        }
-    }
-
-    OutputResult(aError);
-}
-
-void Interpreter::HandleDnsServiceResponse(otError aError, const otDnsServiceResponse *aResponse, void *aContext)
-{
-    static_cast<Interpreter *>(aContext)->HandleDnsServiceResponse(aError, aResponse);
-}
-
-void Interpreter::HandleDnsServiceResponse(otError aError, const otDnsServiceResponse *aResponse)
-{
-    char             name[OT_DNS_MAX_NAME_SIZE];
-    char             label[OT_DNS_MAX_LABEL_SIZE];
-    uint8_t          txtBuffer[kMaxTxtDataSize];
-    otDnsServiceInfo serviceInfo;
-
-    IgnoreError(otDnsServiceResponseGetServiceName(aResponse, label, sizeof(label), name, sizeof(name)));
-
-    OutputLine("DNS service resolution response for %s for service %s", label, name);
-
-    if (aError == OT_ERROR_NONE)
-    {
-        serviceInfo.mHostNameBuffer     = name;
-        serviceInfo.mHostNameBufferSize = sizeof(name);
-        serviceInfo.mTxtData            = txtBuffer;
-        serviceInfo.mTxtDataSize        = sizeof(txtBuffer);
-
-        if (otDnsServiceResponseGetServiceInfo(aResponse, &serviceInfo) == OT_ERROR_NONE)
-        {
-            OutputDnsServiceInfo(/* aIndetSize */ 0, serviceInfo);
-            OutputNewLine();
-        }
-    }
-
-    OutputResult(aError);
-}
-
-#endif // OPENTHREAD_CONFIG_DNS_CLIENT_SERVICE_DISCOVERY_ENABLE
-#endif // OPENTHREAD_CONFIG_DNS_CLIENT_ENABLE
-
 #if OPENTHREAD_FTD
 const char *EidCacheStateToString(otCacheEntryState aState)
 {
@@ -3505,7 +3005,7 @@
 
     memset(&iterator, 0, sizeof(iterator));
 
-    for (uint8_t i = 0;; i++)
+    while (true)
     {
         SuccessOrExit(otThreadGetNextCacheEntry(GetInstancePtr(), &entry, &iterator));
         OutputEidCacheEntry(entry);
@@ -3858,6 +3358,29 @@
     return error;
 }
 
+template <> otError Interpreter::Process<Cmd("instanceid")>(Arg aArgs[])
+{
+    otError error = OT_ERROR_INVALID_ARGS;
+
+    /**
+     * @cli instanceid
+     * @code
+     * instanceid
+     * 468697314
+     * Done
+     * @endcode
+     * @par api_copy
+     * #otInstanceGetId
+     */
+    if (aArgs[0].IsEmpty())
+    {
+        OutputLine("%lu", ToUlong(otInstanceGetId(GetInstancePtr())));
+        error = OT_ERROR_NONE;
+    }
+
+    return error;
+}
+
 const char *Interpreter::AddressOriginToString(uint8_t aOrigin)
 {
     static const char *const kOriginStrings[4] = {
@@ -5280,6 +4803,84 @@
             OutputLine("| %5lu |", ToUlong(neighborInfo.mAge));
         }
     }
+#if OPENTHREAD_CONFIG_UPTIME_ENABLE
+    /**
+     * @cli neighbor conntime
+     * @code
+     * neighbor conntime
+     * | RLOC16 | Extended MAC     | Last Heard (Age) | Connection Time  |
+     * +--------+------------------+------------------+------------------+
+     * | 0x8401 | 1a28be396a14a318 |         00:00:13 |         00:07:59 |
+     * | 0x5c00 | 723ebf0d9eba3264 |         00:00:03 |         00:11:27 |
+     * | 0xe800 | ce53628a1e3f5b3c |         00:00:02 |         00:00:15 |
+     * Done
+     * @endcode
+     * @par
+     * Print the connection time and age of neighbors. Info per neighbor:
+     * - RLOC16
+     * - Extended MAC address
+     * - Last Heard (seconds since last heard from neighbor)
+     * - Connection time (seconds since link establishment with neighbor)
+     * Duration intervals are formatted as `{hh}:{mm}:{ss}` for hours, minutes, and seconds if the duration is less
+     * than one day. If the duration is longer than one day, the format is `{dd}d.{hh}:{mm}:{ss}`.
+     */
+    else if (aArgs[0] == "conntime")
+    {
+        /**
+         * @cli neighbor conntime list
+         * @code
+         * neighbor conntime list
+         * 0x8401 1a28be396a14a318 age:63 conn-time:644
+         * 0x5c00 723ebf0d9eba3264 age:23 conn-time:852
+         * 0xe800 ce53628a1e3f5b3c age:23 conn-time:180
+         * Done
+         * @endcode
+         * @par
+         * Print connection time and age of neighbors.
+         * This command is similar to `neighbor conntime`, but it displays the information in a list format. The age
+         * and connection time are both displayed in seconds.
+         */
+        if (aArgs[1] == "list")
+        {
+            isTable = false;
+        }
+        else
+        {
+            static const char *const kConnTimeTableTitles[] = {
+                "RLOC16",
+                "Extended MAC",
+                "Last Heard (Age)",
+                "Connection Time",
+            };
+
+            static const uint8_t kConnTimeTableColumnWidths[] = {8, 18, 18, 18};
+
+            isTable = true;
+            OutputTableHeader(kConnTimeTableTitles, kConnTimeTableColumnWidths);
+        }
+
+        while (otThreadGetNextNeighborInfo(GetInstancePtr(), &iterator, &neighborInfo) == OT_ERROR_NONE)
+        {
+            if (isTable)
+            {
+                char string[OT_DURATION_STRING_SIZE];
+
+                OutputFormat("| 0x%04x | ", neighborInfo.mRloc16);
+                OutputExtAddress(neighborInfo.mExtAddress);
+                otConvertDurationInSecondsToString(neighborInfo.mAge, string, sizeof(string));
+                OutputFormat(" | %16s", string);
+                otConvertDurationInSecondsToString(neighborInfo.mConnectionTime, string, sizeof(string));
+                OutputLine(" | %16s |", string);
+            }
+            else
+            {
+                OutputFormat("0x%04x ", neighborInfo.mRloc16);
+                OutputExtAddress(neighborInfo.mExtAddress);
+                OutputLine(" age:%lu conn-time:%lu", ToUlong(neighborInfo.mAge), ToUlong(neighborInfo.mConnectionTime));
+            }
+        }
+    }
+#endif
     else
     {
         error = OT_ERROR_INVALID_ARGS;
@@ -5437,24 +5038,18 @@
 }
 #endif
 
+/**
+ * @cli networkname
+ * @code
+ * networkname
+ * OpenThread
+ * Done
+ * @endcode
+ * @par api_copy
+ * #otThreadGetNetworkName
+ */
 template <> otError Interpreter::Process<Cmd("networkname")>(Arg aArgs[])
 {
-    otError error = OT_ERROR_NONE;
-
-    /**
-     * @cli networkname
-     * @code
-     * networkname
-     * OpenThread
-     * Done
-     * @endcode
-     * @par api_copy
-     * #otThreadGetNetworkName
-     */
-    if (aArgs[0].IsEmpty())
-    {
-        OutputLine("%s", otThreadGetNetworkName(GetInstancePtr()));
-    }
     /**
      * @cli networkname (name)
      * @code
@@ -5467,13 +5062,7 @@
      * @par
      * Note: The current commissioning credential becomes stale after changing this value. Use `pskc` to reset.
      */
-    else
-    {
-        SuccessOrExit(error = otThreadSetNetworkName(GetInstancePtr(), aArgs[0].GetCString()));
-    }
-
-exit:
-    return error;
+    return ProcessGetSet(aArgs, otThreadGetNetworkName, otThreadSetNetworkName);
 }
 
 #if OPENTHREAD_CONFIG_TIME_SYNC_ENABLE
@@ -7495,253 +7084,8 @@
 #endif
 
 #if OPENTHREAD_CONFIG_MAC_FILTER_ENABLE
-template <> otError Interpreter::Process<Cmd("macfilter")>(Arg aArgs[])
-{
-    otError error = OT_ERROR_NONE;
-
-    if (aArgs[0].IsEmpty())
-    {
-        PrintMacFilter();
-    }
-    else if (aArgs[0] == "addr")
-    {
-        error = ProcessMacFilterAddress(aArgs + 1);
-    }
-    else if (aArgs[0] == "rss")
-    {
-        error = ProcessMacFilterRss(aArgs + 1);
-    }
-    else
-    {
-        error = OT_ERROR_INVALID_COMMAND;
-    }
-
-    return error;
-}
-
-void Interpreter::PrintMacFilter(void)
-{
-    otMacFilterEntry    entry;
-    otMacFilterIterator iterator = OT_MAC_FILTER_ITERATOR_INIT;
-
-    OutputLine("Address Mode: %s", MacFilterAddressModeToString(otLinkFilterGetAddressMode(GetInstancePtr())));
-
-    while (otLinkFilterGetNextAddress(GetInstancePtr(), &iterator, &entry) == OT_ERROR_NONE)
-    {
-        OutputMacFilterEntry(entry);
-    }
-
-    iterator = OT_MAC_FILTER_ITERATOR_INIT;
-    OutputLine("RssIn List:");
-
-    while (otLinkFilterGetNextRssIn(GetInstancePtr(), &iterator, &entry) == OT_ERROR_NONE)
-    {
-        uint8_t i = 0;
-
-        for (; i < OT_EXT_ADDRESS_SIZE; i++)
-        {
-            if (entry.mExtAddress.m8[i] != 0xff)
-            {
-                break;
-            }
-        }
-
-        if (i == OT_EXT_ADDRESS_SIZE)
-        {
-            OutputLine("Default rss : %d (lqi %u)", entry.mRssIn,
-                       otLinkConvertRssToLinkQuality(GetInstancePtr(), entry.mRssIn));
-        }
-        else
-        {
-            OutputMacFilterEntry(entry);
-        }
-    }
-}
-
-otError Interpreter::ProcessMacFilterAddress(Arg aArgs[])
-{
-    otError      error = OT_ERROR_NONE;
-    otExtAddress extAddr;
-
-    if (aArgs[0].IsEmpty())
-    {
-        otMacFilterIterator iterator = OT_MAC_FILTER_ITERATOR_INIT;
-        otMacFilterEntry    entry;
-
-        OutputLine("%s", MacFilterAddressModeToString(otLinkFilterGetAddressMode(GetInstancePtr())));
-
-        while (otLinkFilterGetNextAddress(GetInstancePtr(), &iterator, &entry) == OT_ERROR_NONE)
-        {
-            OutputMacFilterEntry(entry);
-        }
-    }
-    else if (aArgs[0] == "disable")
-    {
-        VerifyOrExit(aArgs[1].IsEmpty(), error = OT_ERROR_INVALID_ARGS);
-        otLinkFilterSetAddressMode(GetInstancePtr(), OT_MAC_FILTER_ADDRESS_MODE_DISABLED);
-    }
-    else if (aArgs[0] == "allowlist")
-    {
-        VerifyOrExit(aArgs[1].IsEmpty(), error = OT_ERROR_INVALID_ARGS);
-        otLinkFilterSetAddressMode(GetInstancePtr(), OT_MAC_FILTER_ADDRESS_MODE_ALLOWLIST);
-    }
-    else if (aArgs[0] == "denylist")
-    {
-        VerifyOrExit(aArgs[1].IsEmpty(), error = OT_ERROR_INVALID_ARGS);
-        otLinkFilterSetAddressMode(GetInstancePtr(), OT_MAC_FILTER_ADDRESS_MODE_DENYLIST);
-    }
-    else if (aArgs[0] == "add")
-    {
-        SuccessOrExit(error = aArgs[1].ParseAsHexString(extAddr.m8));
-        error = otLinkFilterAddAddress(GetInstancePtr(), &extAddr);
-
-        VerifyOrExit(error == OT_ERROR_NONE || error == OT_ERROR_ALREADY);
-
-        if (!aArgs[2].IsEmpty())
-        {
-            int8_t rss;
-
-            SuccessOrExit(error = aArgs[2].ParseAsInt8(rss));
-            SuccessOrExit(error = otLinkFilterAddRssIn(GetInstancePtr(), &extAddr, rss));
-        }
-    }
-    else if (aArgs[0] == "remove")
-    {
-        SuccessOrExit(error = aArgs[1].ParseAsHexString(extAddr.m8));
-        otLinkFilterRemoveAddress(GetInstancePtr(), &extAddr);
-    }
-    else if (aArgs[0] == "clear")
-    {
-        otLinkFilterClearAddresses(GetInstancePtr());
-    }
-    else
-    {
-        error = OT_ERROR_INVALID_COMMAND;
-    }
-
-exit:
-    return error;
-}
-
-otError Interpreter::ProcessMacFilterRss(Arg aArgs[])
-{
-    otError             error = OT_ERROR_NONE;
-    otMacFilterEntry    entry;
-    otMacFilterIterator iterator = OT_MAC_FILTER_ITERATOR_INIT;
-    otExtAddress        extAddr;
-    int8_t              rss;
-
-    if (aArgs[0].IsEmpty())
-    {
-        while (otLinkFilterGetNextRssIn(GetInstancePtr(), &iterator, &entry) == OT_ERROR_NONE)
-        {
-            uint8_t i = 0;
-
-            for (; i < OT_EXT_ADDRESS_SIZE; i++)
-            {
-                if (entry.mExtAddress.m8[i] != 0xff)
-                {
-                    break;
-                }
-            }
-
-            if (i == OT_EXT_ADDRESS_SIZE)
-            {
-                OutputLine("Default rss: %d (lqi %u)", entry.mRssIn,
-                           otLinkConvertRssToLinkQuality(GetInstancePtr(), entry.mRssIn));
-            }
-            else
-            {
-                OutputMacFilterEntry(entry);
-            }
-        }
-    }
-    else if (aArgs[0] == "add-lqi")
-    {
-        uint8_t linkQuality;
-
-        SuccessOrExit(error = aArgs[2].ParseAsUint8(linkQuality));
-        VerifyOrExit(linkQuality <= 3, error = OT_ERROR_INVALID_ARGS);
-        rss = otLinkConvertLinkQualityToRss(GetInstancePtr(), linkQuality);
-
-        if (aArgs[1] == "*")
-        {
-            otLinkFilterSetDefaultRssIn(GetInstancePtr(), rss);
-        }
-        else
-        {
-            SuccessOrExit(error = aArgs[1].ParseAsHexString(extAddr.m8));
-            error = otLinkFilterAddRssIn(GetInstancePtr(), &extAddr, rss);
-        }
-    }
-    else if (aArgs[0] == "add")
-    {
-        SuccessOrExit(error = aArgs[2].ParseAsInt8(rss));
-
-        if (aArgs[1] == "*")
-        {
-            otLinkFilterSetDefaultRssIn(GetInstancePtr(), rss);
-        }
-        else
-        {
-            SuccessOrExit(error = aArgs[1].ParseAsHexString(extAddr.m8));
-            error = otLinkFilterAddRssIn(GetInstancePtr(), &extAddr, rss);
-        }
-    }
-    else if (aArgs[0] == "remove")
-    {
-        if (aArgs[1] == "*")
-        {
-            otLinkFilterClearDefaultRssIn(GetInstancePtr());
-        }
-        else
-        {
-            SuccessOrExit(error = aArgs[1].ParseAsHexString(extAddr.m8));
-            otLinkFilterRemoveRssIn(GetInstancePtr(), &extAddr);
-        }
-    }
-    else if (aArgs[0] == "clear")
-    {
-        otLinkFilterClearAllRssIn(GetInstancePtr());
-    }
-    else
-    {
-        error = OT_ERROR_INVALID_COMMAND;
-    }
-
-exit:
-    return error;
-}
-
-void Interpreter::OutputMacFilterEntry(const otMacFilterEntry &aEntry)
-{
-    OutputExtAddress(aEntry.mExtAddress);
-
-    if (aEntry.mRssIn != OT_MAC_FILTER_FIXED_RSS_DISABLED)
-    {
-        OutputFormat(" : rss %d (lqi %d)", aEntry.mRssIn,
-                     otLinkConvertRssToLinkQuality(GetInstancePtr(), aEntry.mRssIn));
-    }
-
-    OutputNewLine();
-}
-
-const char *Interpreter::MacFilterAddressModeToString(otMacFilterAddressMode aMode)
-{
-    static const char *const kModeStrings[] = {
-        "Disabled",  // (0) OT_MAC_FILTER_ADDRESS_MODE_DISABLED
-        "Allowlist", // (1) OT_MAC_FILTER_ADDRESS_MODE_ALLOWLIST
-        "Denylist",  // (2) OT_MAC_FILTER_ADDRESS_MODE_DENYLIST
-    };
-
-    static_assert(0 == OT_MAC_FILTER_ADDRESS_MODE_DISABLED, "OT_MAC_FILTER_ADDRESS_MODE_DISABLED value is incorrect");
-    static_assert(1 == OT_MAC_FILTER_ADDRESS_MODE_ALLOWLIST, "OT_MAC_FILTER_ADDRESS_MODE_ALLOWLIST value is incorrect");
-    static_assert(2 == OT_MAC_FILTER_ADDRESS_MODE_DENYLIST, "OT_MAC_FILTER_ADDRESS_MODE_DENYLIST value is incorrect");
-
-    return Stringify(aMode, kModeStrings);
-}
-
-#endif // OPENTHREAD_CONFIG_MAC_FILTER_ENABLE
+template <> otError Interpreter::Process<Cmd("macfilter")>(Arg aArgs[]) { return mMacFilter.Process(aArgs); }
+#endif
 
 template <> otError Interpreter::Process<Cmd("mac")>(Arg aArgs[])
 {
@@ -7881,7 +7225,106 @@
 }
 #endif
 
-#if OPENTHREAD_FTD || OPENTHREAD_CONFIG_TMF_NETWORK_DIAG_MTD_ENABLE
+template <> otError Interpreter::Process<Cmd("vendor")>(Arg aArgs[])
+{
+    Error error = OT_ERROR_INVALID_ARGS;
+
+    /**
+     * @cli vendor name
+     * @code
+     * vendor name
+     * nest
+     * Done
+     * @endcode
+     * @par api_copy
+     * #otThreadGetVendorName
+     */
+    if (aArgs[0] == "name")
+    {
+        aArgs++;
+
+#if !OPENTHREAD_CONFIG_NET_DIAG_VENDOR_INFO_SET_API_ENABLE
+        error = ProcessGet(aArgs, otThreadGetVendorName);
+#else
+        /**
+         * @cli vendor name (name)
+         * @code
+         * vendor name nest
+         * Done
+         * @endcode
+         * @par api_copy
+         * #otThreadSetVendorName
+         * @cparam vendor name @ca{name}
+         */
+        error = ProcessGetSet(aArgs, otThreadGetVendorName, otThreadSetVendorName);
+#endif
+    }
+    /**
+     * @cli vendor model
+     * @code
+     * vendor model
+     * Hub Max
+     * Done
+     * @endcode
+     * @par api_copy
+     * #otThreadGetVendorModel
+     */
+    else if (aArgs[0] == "model")
+    {
+        aArgs++;
+
+#if !OPENTHREAD_CONFIG_NET_DIAG_VENDOR_INFO_SET_API_ENABLE
+        error = ProcessGet(aArgs, otThreadGetVendorModel);
+#else
+        /**
+         * @cli vendor model (name)
+         * @code
+         * vendor model Hub\ Max
+         * Done
+         * @endcode
+         * @par api_copy
+         * #otThreadSetVendorModel
+         * @cparam vendor model @ca{name}
+         */
+        error = ProcessGetSet(aArgs, otThreadGetVendorModel, otThreadSetVendorModel);
+#endif
+    }
+    /**
+     * @cli vendor swversion
+     * @code
+     * vendor swversion
+     * Marble3.5.1
+     * Done
+     * @endcode
+     * @par api_copy
+     * #otThreadGetVendorSwVersion
+     */
+    else if (aArgs[0] == "swversion")
+    {
+        aArgs++;
+
+#if !OPENTHREAD_CONFIG_NET_DIAG_VENDOR_INFO_SET_API_ENABLE
+        error = ProcessGet(aArgs, otThreadGetVendorSwVersion);
+#else
+        /**
+         * @cli vendor swversion (version)
+         * @code
+         * vendor swversion Marble3.5.1
+         * Done
+         * @endcode
+         * @par api_copy
+         * #otThreadSetVendorSwVersion
+         * @cparam vendor swversion @ca{version}
+         */
+        error = ProcessGetSet(aArgs, otThreadGetVendorSwVersion, otThreadSetVendorSwVersion);
+#endif
+    }
+
+    return error;
+}
+
+#if OPENTHREAD_CONFIG_TMF_NETDIAG_CLIENT_ENABLE
+
 template <> otError Interpreter::Process<Cmd("networkdiagnostic")>(Arg aArgs[])
 {
     otError      error = OT_ERROR_NONE;
@@ -8027,6 +7470,20 @@
         case OT_NETWORK_DIAGNOSTIC_TLV_MAX_CHILD_TIMEOUT:
             OutputLine("Max Child Timeout: %lu", ToUlong(diagTlv.mData.mMaxChildTimeout));
             break;
+        case OT_NETWORK_DIAGNOSTIC_TLV_VENDOR_NAME:
+            OutputLine("Vendor Name: %s", diagTlv.mData.mVendorName);
+            break;
+        case OT_NETWORK_DIAGNOSTIC_TLV_VENDOR_MODEL:
+            OutputLine("Vendor Model: %s", diagTlv.mData.mVendorModel);
+            break;
+        case OT_NETWORK_DIAGNOSTIC_TLV_VENDOR_SW_VERSION:
+            OutputLine("Vendor SW Version: %s", diagTlv.mData.mVendorSwVersion);
+            break;
+        case OT_NETWORK_DIAGNOSTIC_TLV_THREAD_STACK_VERSION:
+            OutputLine("Thread Stack Version: %s", diagTlv.mData.mThreadStackVersion);
+            break;
+        default:
+            break;
         }
     }
 
@@ -8106,7 +7563,7 @@
     OutputLine(aIndentSize, "Mode:");
     OutputMode(aIndentSize + kIndentSize, aChildEntry.mMode);
 }
-#endif // OPENTHREAD_FTD || OPENTHREAD_CONFIG_TMF_NETWORK_DIAG_MTD_ENABLE
+#endif // OPENTHREAD_CONFIG_TMF_NETDIAG_CLIENT_ENABLE
 
 void Interpreter::HandleDetachGracefullyResult(void *aContext)
 {
@@ -8258,7 +7715,10 @@
 #endif
 #if OPENTHREAD_FTD || OPENTHREAD_MTD
         CmdEntry("discover"),
+#if OPENTHREAD_CONFIG_DNS_CLIENT_ENABLE || OPENTHREAD_CONFIG_DNSSD_SERVER_ENABLE || \
+    OPENTHREAD_CONFIG_REFERENCE_DEVICE_ENABLE
         CmdEntry("dns"),
+#endif
 #if (OPENTHREAD_CONFIG_THREAD_VERSION >= OT_THREAD_VERSION_1_2)
         CmdEntry("domainname"),
 #endif
@@ -8282,6 +7742,7 @@
         CmdEntry("history"),
 #endif
         CmdEntry("ifconfig"),
+        CmdEntry("instanceid"),
         CmdEntry("ipaddr"),
         CmdEntry("ipmaddr"),
 #if OPENTHREAD_CONFIG_JOINER_ENABLE
@@ -8325,7 +7786,7 @@
 #endif
         CmdEntry("netdata"),
         CmdEntry("netstat"),
-#if OPENTHREAD_FTD || OPENTHREAD_CONFIG_TMF_NETWORK_DIAG_MTD_ENABLE
+#if OPENTHREAD_CONFIG_TMF_NETDIAG_CLIENT_ENABLE
         CmdEntry("networkdiagnostic"),
 #endif
 #if OPENTHREAD_FTD
@@ -8419,6 +7880,7 @@
 #if OPENTHREAD_CONFIG_UPTIME_ENABLE
         CmdEntry("uptime"),
 #endif
+        CmdEntry("vendor"),
 #endif // OPENTHREAD_FTD || OPENTHREAD_MTD
         CmdEntry("version"),
     };
@@ -8438,9 +7900,12 @@
     {
         OutputCommandTable(kCommands);
 
-        for (uint8_t i = 0; i < mUserCommandsLength; i++)
+        for (const UserCommandsEntry &entry : mUserCommands)
         {
-            OutputLine("%s", mUserCommands[i].mName);
+            for (uint8_t i = 0; i < entry.mLength; i++)
+            {
+                OutputLine("%s", entry.mCommands[i].mName);
+            }
         }
     }
     else
@@ -8454,13 +7919,17 @@
 extern "C" void otCliInit(otInstance *aInstance, otCliOutputCallback aCallback, void *aContext)
 {
     Interpreter::Initialize(aInstance, aCallback, aContext);
+
+#if OPENTHREAD_CONFIG_CLI_VENDOR_COMMANDS_ENABLE && OPENTHREAD_CONFIG_CLI_MAX_USER_CMD_ENTRIES > 1
+    otCliVendorSetUserCommands();
+#endif
 }
 
 extern "C" void otCliInputLine(char *aBuf) { Interpreter::GetInterpreter().ProcessLine(aBuf); }
 
-extern "C" void otCliSetUserCommands(const otCliCommand *aUserCommands, uint8_t aLength, void *aContext)
+extern "C" otError otCliSetUserCommands(const otCliCommand *aUserCommands, uint8_t aLength, void *aContext)
 {
-    Interpreter::GetInterpreter().SetUserCommands(aUserCommands, aLength, aContext);
+    return Interpreter::GetInterpreter().SetUserCommands(aUserCommands, aLength, aContext);
 }
 
 extern "C" void otCliOutputBytes(const uint8_t *aBytes, uint8_t aLength)
diff --git a/src/cli/cli.hpp b/src/cli/cli.hpp
index bb220a8..26c2614 100644
--- a/src/cli/cli.hpp
+++ b/src/cli/cli.hpp
@@ -61,8 +61,10 @@
 #include "cli/cli_br.hpp"
 #include "cli/cli_commissioner.hpp"
 #include "cli/cli_dataset.hpp"
+#include "cli/cli_dns.hpp"
 #include "cli/cli_history.hpp"
 #include "cli/cli_joiner.hpp"
+#include "cli/cli_mac_filter.hpp"
 #include "cli/cli_network_data.hpp"
 #include "cli/cli_output.hpp"
 #include "cli/cli_srp_client.hpp"
@@ -76,6 +78,7 @@
 #include "cli/cli_coap_secure.hpp"
 #endif
 
+#include "common/array.hpp"
 #include "common/code_utils.hpp"
 #include "common/debug.hpp"
 #include "common/instance.hpp"
@@ -106,6 +109,7 @@
 #if OPENTHREAD_FTD || OPENTHREAD_MTD
     friend class Br;
     friend class Commissioner;
+    friend class Dns;
     friend class Joiner;
     friend class NetworkData;
     friend class SrpClient;
@@ -180,14 +184,16 @@
     static otError ParseEnableOrDisable(const Arg &aArg, bool &aEnable);
 
     /**
-     * This method sets the user command table.
+     * This method adds commands to the user command table.
      *
      * @param[in]  aCommands  A pointer to an array with user commands.
      * @param[in]  aLength    @p aUserCommands length.
      * @param[in]  aContext   @p aUserCommands length.
      *
+     * @retval OT_ERROR_NONE    Successfully updated command table with commands from @p aCommands.
+     * @retval OT_ERROR_FAILED  No available UserCommandsEntry to register requested user commands.
      */
-    void SetUserCommands(const otCliCommand *aCommands, uint8_t aLength, void *aContext);
+    otError SetUserCommands(const otCliCommand *aCommands, uint8_t aLength, void *aContext);
 
     static constexpr uint8_t kLinkModeStringSize = sizeof("rdn"); ///< Size of string buffer for a MLE Link Mode.
 
@@ -265,10 +271,11 @@
 private:
     enum
     {
-        kIndentSize       = 4,
-        kMaxArgs          = 32,
-        kMaxAutoAddresses = 8,
-        kMaxLineLength    = OPENTHREAD_CONFIG_CLI_MAX_LINE_LENGTH,
+        kIndentSize            = 4,
+        kMaxArgs               = 32,
+        kMaxAutoAddresses      = 8,
+        kMaxLineLength         = OPENTHREAD_CONFIG_CLI_MAX_LINE_LENGTH,
+        kMaxUserCommandEntries = OPENTHREAD_CONFIG_CLI_MAX_USER_CMD_ENTRIES,
     };
 
     static constexpr uint32_t kNetworkDiagnosticTimeoutMsecs = 5000;
@@ -285,14 +292,15 @@
     // Returns format string to output a `ValueType` (e.g., "%u" for `uint16_t`).
     template <typename ValueType> static constexpr const char *FormatStringFor(void);
 
-    // General temaplate implementaion.
+    // General template implementation.
     // Specializations for `uint32_t` and `int32_t` are added at the end.
     template <typename ValueType> otError ProcessGet(Arg aArgs[], GetHandler<ValueType> aGetHandler)
     {
         static_assert(
             TypeTraits::IsSame<ValueType, uint8_t>::kValue || TypeTraits::IsSame<ValueType, uint16_t>::kValue ||
-                TypeTraits::IsSame<ValueType, int8_t>::kValue || TypeTraits::IsSame<ValueType, int16_t>::kValue,
-            "ValueType must be an  8, 16 `int` or `uint` type");
+                TypeTraits::IsSame<ValueType, int8_t>::kValue || TypeTraits::IsSame<ValueType, int16_t>::kValue ||
+                TypeTraits::IsSame<ValueType, const char *>::kValue,
+            "ValueType must be an  8, 16 `int` or `uint` type, or a `const char *`");
 
         otError error = OT_ERROR_NONE;
 
@@ -423,14 +431,6 @@
     void OutputMultiRadioInfo(const otMultiRadioNeighborInfo &aMultiRadioInfo);
 #endif
 
-#if OPENTHREAD_CONFIG_MAC_FILTER_ENABLE
-    void               PrintMacFilter(void);
-    otError            ProcessMacFilterAddress(Arg aArgs[]);
-    otError            ProcessMacFilterRss(Arg aArgs[]);
-    void               OutputMacFilterEntry(const otMacFilterEntry &aEntry);
-    static const char *MacFilterAddressModeToString(otMacFilterAddressMode aMode);
-#endif
-
 #if OPENTHREAD_CONFIG_PING_SENDER_ENABLE
     static void HandlePingReply(const otPingSenderReply *aReply, void *aContext);
     static void HandlePingStatistics(const otPingSenderStatistics *aStatistics, void *aContext);
@@ -439,7 +439,7 @@
     static void HandleEnergyScanResult(otEnergyScanResult *aResult, void *aContext);
     static void HandleLinkPcapReceive(const otRadioFrame *aFrame, bool aIsTx, void *aContext);
 
-#if OPENTHREAD_FTD || (OPENTHREAD_MTD && OPENTHREAD_CONFIG_TMF_NETWORK_DIAG_MTD_ENABLE)
+#if OPENTHREAD_CONFIG_TMF_NETDIAG_CLIENT_ENABLE
     void HandleDiagnosticGetResponse(otError aError, const otMessage *aMessage, const Ip6::MessageInfo *aMessageInfo);
     static void HandleDiagnosticGetResponse(otError              aError,
                                             otMessage           *aMessage,
@@ -455,17 +455,8 @@
     void OutputChildTableEntry(uint8_t aIndentSize, const otNetworkDiagChildEntry &aChildEntry);
 #endif
 
-#if OPENTHREAD_CONFIG_DNS_CLIENT_ENABLE
-    otError     GetDnsConfig(Arg aArgs[], otDnsQueryConfig *&aConfig);
-    static void HandleDnsAddressResponse(otError aError, const otDnsAddressResponse *aResponse, void *aContext);
-    void        HandleDnsAddressResponse(otError aError, const otDnsAddressResponse *aResponse);
-#if OPENTHREAD_CONFIG_DNS_CLIENT_SERVICE_DISCOVERY_ENABLE
-    void        OutputDnsServiceInfo(uint8_t aIndentSize, const otDnsServiceInfo &aServiceInfo);
-    static void HandleDnsBrowseResponse(otError aError, const otDnsBrowseResponse *aResponse, void *aContext);
-    void        HandleDnsBrowseResponse(otError aError, const otDnsBrowseResponse *aResponse);
-    static void HandleDnsServiceResponse(otError aError, const otDnsServiceResponse *aResponse, void *aContext);
-    void        HandleDnsServiceResponse(otError aError, const otDnsServiceResponse *aResponse);
-#endif
+#if OPENTHREAD_CONFIG_NAT64_TRANSLATOR_ENABLE
+    void OutputNat64Counters(const otNat64Counters &aCounters);
 #endif
 
 #if OPENTHREAD_CONFIG_SNTP_CLIENT_ENABLE
@@ -529,10 +520,15 @@
     static void HandleTimer(Timer &aTimer);
     void        HandleTimer(void);
 
-    const otCliCommand *mUserCommands;
-    uint8_t             mUserCommandsLength;
-    void               *mUserCommandsContext;
-    bool                mCommandIsPending;
+    struct UserCommandsEntry
+    {
+        const otCliCommand *mCommands;
+        uint8_t             mLength;
+        void               *mContext;
+    };
+
+    UserCommandsEntry mUserCommands[kMaxUserCommandEntries];
+    bool              mCommandIsPending;
 
     TimerMilliContext mTimer;
 
@@ -545,6 +541,14 @@
     NetworkData mNetworkData;
     UdpExample  mUdp;
 
+#if OPENTHREAD_CONFIG_MAC_FILTER_ENABLE
+    MacFilter mMacFilter;
+#endif
+
+#if OPENTHREAD_CLI_DNS_ENABLE
+    Dns mDns;
+#endif
+
 #if OPENTHREAD_CONFIG_BORDER_ROUTING_ENABLE
     Br mBr;
 #endif
@@ -608,6 +612,8 @@
 
 template <> inline constexpr const char *Interpreter::FormatStringFor<int32_t>(void) { return "%ld"; }
 
+template <> inline constexpr const char *Interpreter::FormatStringFor<const char *>(void) { return "%s"; }
+
 // Specialization of ProcessGet<> for `uint32_t` and `int32_t`
 
 template <> inline otError Interpreter::ProcessGet<uint32_t>(Arg aArgs[], GetHandler<uint32_t> aGetHandler)
diff --git a/src/cli/cli_br.cpp b/src/cli/cli_br.cpp
index 643b435..a3e2152 100644
--- a/src/cli/cli_br.cpp
+++ b/src/cli/cli_br.cpp
@@ -119,27 +119,25 @@
     return error;
 }
 
-otError Br::ParsePrefixTypeArgs(Arg aArgs[], bool &aOutputLocal, bool &aOutputFavored)
+otError Br::ParsePrefixTypeArgs(Arg aArgs[], PrefixType &aFlags)
 {
     otError error = OT_ERROR_NONE;
 
-    aOutputLocal   = false;
-    aOutputFavored = false;
+    aFlags = 0;
 
     if (aArgs[0].IsEmpty())
     {
-        aOutputLocal   = true;
-        aOutputFavored = true;
+        aFlags = kPrefixTypeFavored | kPrefixTypeLocal;
         ExitNow();
     }
 
     if (aArgs[0] == "local")
     {
-        aOutputLocal = true;
+        aFlags = kPrefixTypeLocal;
     }
     else if (aArgs[0] == "favored")
     {
-        aOutputFavored = true;
+        aFlags = kPrefixTypeFavored;
     }
     else
     {
@@ -167,11 +165,10 @@
  */
 template <> otError Br::Process<Cmd("omrprefix")>(Arg aArgs[])
 {
-    otError error = OT_ERROR_NONE;
-    bool    outputLocal;
-    bool    outputFavored;
+    otError    error = OT_ERROR_NONE;
+    PrefixType outputPrefixTypes;
 
-    SuccessOrExit(error = ParsePrefixTypeArgs(aArgs, outputLocal, outputFavored));
+    SuccessOrExit(error = ParsePrefixTypeArgs(aArgs, outputPrefixTypes));
 
     /**
      * @cli br omrprefix local
@@ -183,13 +180,13 @@
      * @par api_copy
      * #otBorderRoutingGetOmrPrefix
      */
-    if (outputLocal)
+    if (outputPrefixTypes & kPrefixTypeLocal)
     {
         otIp6Prefix local;
 
         SuccessOrExit(error = otBorderRoutingGetOmrPrefix(GetInstancePtr(), &local));
 
-        OutputFormat("%s", outputFavored ? "Local: " : "");
+        OutputFormat("%s", outputPrefixTypes == kPrefixTypeLocal ? "" : "Local: ");
         OutputIp6PrefixLine(local);
     }
 
@@ -203,14 +200,14 @@
      * @par api_copy
      * #otBorderRoutingGetFavoredOmrPrefix
      */
-    if (outputFavored)
+    if (outputPrefixTypes & kPrefixTypeFavored)
     {
         otIp6Prefix       favored;
         otRoutePreference preference;
 
         SuccessOrExit(error = otBorderRoutingGetFavoredOmrPrefix(GetInstancePtr(), &favored, &preference));
 
-        OutputFormat("%s", outputLocal ? "Favored: " : "");
+        OutputFormat("%s", outputPrefixTypes == kPrefixTypeFavored ? "" : "Favored: ");
         OutputIp6Prefix(favored);
         OutputLine(" prf:%s", Interpreter::PreferenceToString(preference));
     }
@@ -234,11 +231,10 @@
  */
 template <> otError Br::Process<Cmd("onlinkprefix")>(Arg aArgs[])
 {
-    otError error = OT_ERROR_NONE;
-    bool    outputLocal;
-    bool    outputFavored;
+    otError    error = OT_ERROR_NONE;
+    PrefixType outputPrefixTypes;
 
-    SuccessOrExit(error = ParsePrefixTypeArgs(aArgs, outputLocal, outputFavored));
+    SuccessOrExit(error = ParsePrefixTypeArgs(aArgs, outputPrefixTypes));
 
     /**
      * @cli br onlinkprefix local
@@ -250,13 +246,13 @@
      * @par api_copy
      * #otBorderRoutingGetOnLinkPrefix
      */
-    if (outputLocal)
+    if (outputPrefixTypes & kPrefixTypeLocal)
     {
         otIp6Prefix local;
 
         SuccessOrExit(error = otBorderRoutingGetOnLinkPrefix(GetInstancePtr(), &local));
 
-        OutputFormat("%s", outputFavored ? "Local: " : "");
+        OutputFormat("%s", outputPrefixTypes == kPrefixTypeLocal ? "" : "Local: ");
         OutputIp6PrefixLine(local);
     }
 
@@ -270,13 +266,13 @@
      * @par api_copy
      * #otBorderRoutingGetFavoredOnLinkPrefix
      */
-    if (outputFavored)
+    if (outputPrefixTypes & kPrefixTypeFavored)
     {
         otIp6Prefix favored;
 
         SuccessOrExit(error = otBorderRoutingGetFavoredOnLinkPrefix(GetInstancePtr(), &favored));
 
-        OutputFormat("%s", outputLocal ? "Favored: " : "");
+        OutputFormat("%s", outputPrefixTypes == kPrefixTypeFavored ? "" : "Favored: ");
         OutputIp6PrefixLine(favored);
     }
 
@@ -301,11 +297,10 @@
  */
 template <> otError Br::Process<Cmd("nat64prefix")>(Arg aArgs[])
 {
-    otError error = OT_ERROR_NONE;
-    bool    outputLocal;
-    bool    outputFavored;
+    otError    error = OT_ERROR_NONE;
+    PrefixType outputPrefixTypes;
 
-    SuccessOrExit(error = ParsePrefixTypeArgs(aArgs, outputLocal, outputFavored));
+    SuccessOrExit(error = ParsePrefixTypeArgs(aArgs, outputPrefixTypes));
 
     /**
      * @cli br nat64prefix local
@@ -317,13 +312,13 @@
      * @par api_copy
      * #otBorderRoutingGetNat64Prefix
      */
-    if (outputLocal)
+    if (outputPrefixTypes & kPrefixTypeLocal)
     {
         otIp6Prefix local;
 
         SuccessOrExit(error = otBorderRoutingGetNat64Prefix(GetInstancePtr(), &local));
 
-        OutputFormat("%s", outputFavored ? "Local: " : "");
+        OutputFormat("%s", outputPrefixTypes == kPrefixTypeLocal ? "" : "Local: ");
         OutputIp6PrefixLine(local);
     }
 
@@ -337,14 +332,14 @@
      * @par api_copy
      * #otBorderRoutingGetFavoredNat64Prefix
      */
-    if (outputFavored)
+    if (outputPrefixTypes & kPrefixTypeFavored)
     {
         otIp6Prefix       favored;
         otRoutePreference preference;
 
         SuccessOrExit(error = otBorderRoutingGetFavoredNat64Prefix(GetInstancePtr(), &favored, &preference));
 
-        OutputFormat("%s", outputLocal ? "Favored: " : "");
+        OutputFormat("%s", outputPrefixTypes == kPrefixTypeFavored ? "" : "Favored: ");
         OutputIp6Prefix(favored);
         OutputLine(" prf:%s", Interpreter::PreferenceToString(preference));
     }
diff --git a/src/cli/cli_br.hpp b/src/cli/cli_br.hpp
index 5a30977..794f874 100644
--- a/src/cli/cli_br.hpp
+++ b/src/cli/cli_br.hpp
@@ -76,9 +76,16 @@
 private:
     using Command = CommandEntry<Br>;
 
+    using PrefixType = uint8_t;
+    enum : PrefixType
+    {
+        kPrefixTypeLocal   = 1u << 0,
+        kPrefixTypeFavored = 1u << 1,
+    };
+
     template <CommandId kCommandId> otError Process(Arg aArgs[]);
 
-    otError ParsePrefixTypeArgs(Arg aArgs[], bool &aOutputLocal, bool &aOutputFavored);
+    otError ParsePrefixTypeArgs(Arg aArgs[], PrefixType &aFlags);
 };
 
 } // namespace Cli
diff --git a/src/cli/cli_config.h b/src/cli/cli_config.h
index 15b444b..c3762c4 100644
--- a/src/cli/cli_config.h
+++ b/src/cli/cli_config.h
@@ -88,6 +88,28 @@
 #endif
 
 /**
+ * @def OPENTHREAD_CONFIG_CLI_MAX_USER_CMD_ENTRIES
+ *
+ * The maximum number of user CLI command lists that can be registered by the interpreter.
+ *
+ */
+#ifndef OPENTHREAD_CONFIG_CLI_MAX_USER_CMD_ENTRIES
+#define OPENTHREAD_CONFIG_CLI_MAX_USER_CMD_ENTRIES 1
+#endif
+
+/**
+ * @def OPENTHREAD_CONFIG_CLI_VENDOR_COMMANDS_ENABLE
+ *
+ * Indicates whether or not an externally provided list of cli commands is defined.
+ *
+ * This is to be used only when `OPENTHREAD_CONFIG_CLI_MAX_USER_CMD_ENTRIES` is greater than 1.
+ *
+ */
+#ifndef OPENTHREAD_CONFIG_CLI_VENDOR_COMMANDS_ENABLE
+#define OPENTHREAD_CONFIG_CLI_VENDOR_COMMANDS_ENABLE 0
+#endif
+
+/**
  * @def OPENTHREAD_CONFIG_CLI_LOG_INPUT_OUTPUT_ENABLE
  *
  * Define as 1 for CLI to emit its command input string and the resulting output to the logs.
diff --git a/src/cli/cli_dns.cpp b/src/cli/cli_dns.cpp
new file mode 100644
index 0000000..c85cb24
--- /dev/null
+++ b/src/cli/cli_dns.cpp
@@ -0,0 +1,762 @@
+/*
+ *  Copyright (c) 2023, The OpenThread Authors.
+ *  All rights reserved.
+ *
+ *  Redistribution and use in source and binary forms, with or without
+ *  modification, are permitted provided that the following conditions are met:
+ *  1. Redistributions of source code must retain the above copyright
+ *     notice, this list of conditions and the following disclaimer.
+ *  2. Redistributions in binary form must reproduce the above copyright
+ *     notice, this list of conditions and the following disclaimer in the
+ *     documentation and/or other materials provided with the distribution.
+ *  3. Neither the name of the copyright holder nor the
+ *     names of its contributors may be used to endorse or promote products
+ *     derived from this software without specific prior written permission.
+ *
+ *  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ *  AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ *  IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ *  ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ *  LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ *  CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ *  SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ *  INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ *  CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ *  ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ *  POSSIBILITY OF SUCH DAMAGE.
+ */
+
+/**
+ * @file
+ *   This file implements CLI for DNS (client and server/resolver).
+ */
+
+#include "cli_dns.hpp"
+
+#include "cli/cli.hpp"
+
+#if OPENTHREAD_CLI_DNS_ENABLE
+
+namespace ot {
+namespace Cli {
+
+#if OPENTHREAD_CONFIG_REFERENCE_DEVICE_ENABLE
+
+template <> otError Dns::Process<Cmd("compression")>(Arg aArgs[])
+{
+    otError error = OT_ERROR_NONE;
+
+    /**
+     * @cli dns compression
+     * @code
+     * dns compression
+     * Enabled
+     * @endcode
+     * @cparam dns compression [@ca{enable|disable}]
+     * @par api_copy
+     * #otDnsIsNameCompressionEnabled
+     * @par
+     * By default DNS name compression is enabled. When disabled,
+     * DNS names are appended as full and never compressed. This
+     * is applicable to OpenThread's DNS and SRP client/server
+     * modules."
+     * `OPENTHREAD_CONFIG_REFERENCE_DEVICE_ENABLE` is required.
+     */
+    if (aArgs[0].IsEmpty())
+    {
+        OutputEnabledDisabledStatus(otDnsIsNameCompressionEnabled());
+    }
+    /**
+     * @cli dns compression (enable,disable)
+     * @code
+     * dns compression enable
+     * Enabled
+     * @endcode
+     * @code
+     * dns compression disable
+     * Done
+     * dns compression
+     * Disabled
+     * Done
+     * @endcode
+     * @cparam dns compression [@ca{enable|disable}]
+     * @par
+     * Set the "DNS name compression" mode.
+     * @par
+     * By default DNS name compression is enabled. When disabled,
+     * DNS names are appended as full and never compressed. This
+     * is applicable to OpenThread's DNS and SRP client/server
+     * modules."
+     * `OPENTHREAD_CONFIG_REFERENCE_DEVICE_ENABLE` is required.
+     * @sa otDnsSetNameCompressionEnabled
+     */
+    else
+    {
+        bool enable;
+
+        SuccessOrExit(error = Interpreter::ParseEnableOrDisable(aArgs[0], enable));
+        otDnsSetNameCompressionEnabled(enable);
+    }
+
+exit:
+    return error;
+}
+
+#endif // OPENTHREAD_CONFIG_REFERENCE_DEVICE_ENABLE
+
+#if OPENTHREAD_CONFIG_DNS_CLIENT_ENABLE
+
+template <> otError Dns::Process<Cmd("config")>(Arg aArgs[])
+{
+    otError error = OT_ERROR_NONE;
+
+    /**
+     * @cli dns config
+     * @code
+     * dns config
+     * Server: [fd00:0:0:0:0:0:0:1]:1234
+     * ResponseTimeout: 5000 ms
+     * MaxTxAttempts: 2
+     * RecursionDesired: no
+     * ServiceMode: srv
+     * Nat64Mode: allow
+     * Done
+     * @endcode
+     * @par api_copy
+     * #otDnsClientGetDefaultConfig
+     * @par
+     * The config includes the server IPv6 address and port, response
+     * timeout in msec (wait time to rx response), maximum tx attempts
+     * before reporting failure, boolean flag to indicate whether the server
+     * can resolve the query recursively or not.
+     * `OPENTHREAD_CONFIG_DNS_CLIENT_ENABLE` is required.
+     */
+    if (aArgs[0].IsEmpty())
+    {
+        const otDnsQueryConfig *defaultConfig = otDnsClientGetDefaultConfig(GetInstancePtr());
+
+        OutputFormat("Server: ");
+        OutputSockAddrLine(defaultConfig->mServerSockAddr);
+        OutputLine("ResponseTimeout: %lu ms", ToUlong(defaultConfig->mResponseTimeout));
+        OutputLine("MaxTxAttempts: %u", defaultConfig->mMaxTxAttempts);
+        OutputLine("RecursionDesired: %s",
+                   (defaultConfig->mRecursionFlag == OT_DNS_FLAG_RECURSION_DESIRED) ? "yes" : "no");
+        OutputLine("ServiceMode: %s", DnsConfigServiceModeToString(defaultConfig->mServiceMode));
+#if OPENTHREAD_CONFIG_DNS_CLIENT_NAT64_ENABLE
+        OutputLine("Nat64Mode: %s", (defaultConfig->mNat64Mode == OT_DNS_NAT64_ALLOW) ? "allow" : "disallow");
+#endif
+#if OPENTHREAD_CONFIG_DNS_CLIENT_OVER_TCP_ENABLE
+        OutputLine("TransportProtocol: %s", (defaultConfig->mTransportProto == OT_DNS_TRANSPORT_UDP) ? "udp" : "tcp");
+#endif
+    }
+    /**
+     * @cli dns config (set)
+     * @code
+     * dns config fd00::1 1234 5000 2 0
+     * Done
+     * @endcode
+     * @code
+     * dns config
+     * Server: [fd00:0:0:0:0:0:0:1]:1234
+     * ResponseTimeout: 5000 ms
+     * MaxTxAttempts: 2
+     * RecursionDesired: no
+     * Done
+     * @endcode
+     * @code
+     * dns config fd00::2
+     * Done
+     * @endcode
+     * @code
+     * dns config
+     * Server: [fd00:0:0:0:0:0:0:2]:53
+     * ResponseTimeout: 3000 ms
+     * MaxTxAttempts: 3
+     * RecursionDesired: yes
+     * Done
+     * @endcode
+     * @par api_copy
+     * #otDnsClientSetDefaultConfig
+     * @cparam dns config [@ca{dns-server-IP}] [@ca{dns-server-port}] <!--
+     * -->                [@ca{response-timeout-ms}] [@ca{max-tx-attempts}] <!--
+     * -->                [@ca{recursion-desired-boolean}] [@ca{service-mode}]
+     * @par
+     * We can leave some of the fields as unspecified (or use value zero). The
+     * unspecified fields are replaced by the corresponding OT config option
+     * definitions `OPENTHREAD_CONFIG_DNS_CLIENT_DEFAULT` to form the default
+     * query config.
+     * `OPENTHREAD_CONFIG_DNS_CLIENT_ENABLE` is required.
+     */
+    else
+    {
+        otDnsQueryConfig  queryConfig;
+        otDnsQueryConfig *config = &queryConfig;
+
+        SuccessOrExit(error = GetDnsConfig(aArgs, config));
+        otDnsClientSetDefaultConfig(GetInstancePtr(), config);
+    }
+
+exit:
+    return error;
+}
+
+/**
+ * @cli dns resolve
+ * @code
+ * dns resolve ipv6.google.com
+ * DNS response for ipv6.google.com - 2a00:1450:401b:801:0:0:0:200e TTL: 300
+ * @endcode
+ * @code
+ * dns resolve example.com 8.8.8.8
+ * Synthesized IPv6 DNS server address: fdde:ad00:beef:2:0:0:808:808
+ * DNS response for example.com. - fd4c:9574:3720:2:0:0:5db8:d822 TTL:20456
+ * Done
+ * @endcode
+ * @cparam dns resolve @ca{hostname} [@ca{dns-server-IP}] <!--
+ * -->                 [@ca{dns-server-port}] [@ca{response-timeout-ms}] <!--
+ * -->                 [@ca{max-tx-attempts}] [@ca{recursion-desired-boolean}]
+ * @par api_copy
+ * #otDnsClientResolveAddress
+ * @par
+ * Send DNS Query to obtain IPv6 address for given hostname.
+ * @par
+ * The parameters after hostname are optional. Any unspecified (or zero) value
+ * for these optional parameters is replaced by the value from the current default
+ * config (dns config).
+ * @par
+ * The DNS server IP can be an IPv4 address, which will be synthesized to an
+ * IPv6 address using the preferred NAT64 prefix from the network data.
+ * @par
+ * Note: The command will return InvalidState when the DNS server IP is an IPv4
+ * address but the preferred NAT64 prefix is unavailable.
+ * `OPENTHREAD_CONFIG_DNS_CLIENT_ENABLE` is required.
+ */
+template <> otError Dns::Process<Cmd("resolve")>(Arg aArgs[])
+{
+    otError           error = OT_ERROR_NONE;
+    otDnsQueryConfig  queryConfig;
+    otDnsQueryConfig *config = &queryConfig;
+
+    VerifyOrExit(!aArgs[0].IsEmpty(), error = OT_ERROR_INVALID_ARGS);
+    SuccessOrExit(error = GetDnsConfig(aArgs + 1, config));
+    SuccessOrExit(error = otDnsClientResolveAddress(GetInstancePtr(), aArgs[0].GetCString(), &HandleDnsAddressResponse,
+                                                    this, config));
+    error = OT_ERROR_PENDING;
+
+exit:
+    return error;
+}
+
+#if OPENTHREAD_CONFIG_DNS_CLIENT_NAT64_ENABLE
+template <> otError Dns::Process<Cmd("resolve4")>(Arg aArgs[])
+{
+    otError           error = OT_ERROR_NONE;
+    otDnsQueryConfig  queryConfig;
+    otDnsQueryConfig *config = &queryConfig;
+
+    VerifyOrExit(!aArgs[0].IsEmpty(), error = OT_ERROR_INVALID_ARGS);
+    SuccessOrExit(error = GetDnsConfig(aArgs + 1, config));
+    SuccessOrExit(error = otDnsClientResolveIp4Address(GetInstancePtr(), aArgs[0].GetCString(),
+                                                       &HandleDnsAddressResponse, this, config));
+    error = OT_ERROR_PENDING;
+
+exit:
+    return error;
+}
+#endif
+
+#if OPENTHREAD_CONFIG_DNS_CLIENT_SERVICE_DISCOVERY_ENABLE
+
+/**
+ * @cli dns browse
+ * @code
+ * dns browse _service._udp.example.com
+ * DNS browse response for _service._udp.example.com.
+ * inst1
+ *     Port:1234, Priority:1, Weight:2, TTL:7200
+ *     Host:host.example.com.
+ *     HostAddress:fd00:0:0:0:0:0:0:abcd TTL:7200
+ *     TXT:[a=6531, b=6c12] TTL:7300
+ * instance2
+ *     Port:1234, Priority:1, Weight:2, TTL:7200
+ *     Host:host.example.com.
+ *     HostAddress:fd00:0:0:0:0:0:0:abcd TTL:7200
+ *     TXT:[a=1234] TTL:7300
+ * Done
+ * @endcode
+ * @code
+ * dns browse _airplay._tcp.default.service.arpa
+ * DNS browse response for _airplay._tcp.default.service.arpa.
+ * Mac mini
+ *     Port:7000, Priority:0, Weight:0, TTL:10
+ *     Host:Mac-mini.default.service.arpa.
+ *     HostAddress:fd97:739d:386a:1:1c2e:d83c:fcbe:9cf4 TTL:10
+ * Done
+ * @endcode
+ * @cparam dns browse @ca{service-name} [@ca{dns-server-IP}] [@ca{dns-server-port}] <!--
+ * -->                [@ca{response-timeout-ms}] [@ca{max-tx-attempts}] <!--
+ * -->                [@ca{recursion-desired-boolean}]
+ * @sa otDnsClientBrowse
+ * @par
+ * Send a browse (service instance enumeration) DNS query to get the list of services for
+ * given service-name
+ * @par
+ * The parameters after `service-name` are optional. Any unspecified (or zero) value
+ * for these optional parameters is replaced by the value from the current default
+ * config (`dns config`).
+ * @par
+ * Note: The DNS server IP can be an IPv4 address, which will be synthesized to an IPv6
+ * address using the preferred NAT64 prefix from the network data. The command will return
+ * `InvalidState` when the DNS server IP is an IPv4 address but the preferred NAT64 prefix
+ * is unavailable. When testing DNS-SD discovery proxy, the zone is not `local` and
+ * instead should be `default.service.arpa`.
+ * `OPENTHREAD_CONFIG_DNS_CLIENT_SERVICE_DISCOVERY_ENABLE` is required.
+ */
+template <> otError Dns::Process<Cmd("browse")>(Arg aArgs[])
+{
+    otError           error = OT_ERROR_NONE;
+    otDnsQueryConfig  queryConfig;
+    otDnsQueryConfig *config = &queryConfig;
+
+    VerifyOrExit(!aArgs[0].IsEmpty(), error = OT_ERROR_INVALID_ARGS);
+    SuccessOrExit(error = GetDnsConfig(aArgs + 1, config));
+    SuccessOrExit(
+        error = otDnsClientBrowse(GetInstancePtr(), aArgs[0].GetCString(), &HandleDnsBrowseResponse, this, config));
+    error = OT_ERROR_PENDING;
+
+exit:
+    return error;
+}
+
+/**
+ * @cli dns service
+ * @cparam dns service @ca{service-instance-label} @ca{service-name} <!--
+ * -->                 [@ca{DNS-server-IP}] [@ca{DNS-server-port}] <!--
+ * -->                 [@ca{response-timeout-ms}] [@ca{max-tx-attempts}] <!--
+ * -->                 [@ca{recursion-desired-boolean}]
+ * @par api_copy
+ * #otDnsClientResolveService
+ * @par
+ * Send a service instance resolution DNS query for a given service instance.
+ * Service instance label is provided first, followed by the service name
+ * (note that service instance label can contain dot '.' character).
+ * @par
+ * The parameters after `service-name` are optional. Any unspecified (or zero)
+ * value for these optional parameters is replaced by the value from the
+ * current default config (`dns config`).
+ * @par
+ * Note: The DNS server IP can be an IPv4 address, which will be synthesized
+ * to an IPv6 address using the preferred NAT64 prefix from the network data.
+ * The command will return `InvalidState` when the DNS server IP is an IPv4
+ * address but the preferred NAT64 prefix is unavailable.
+ * `OPENTHREAD_CONFIG_DNS_CLIENT_SERVICE_DISCOVERY_ENABLE` is required.
+ */
+template <> otError Dns::Process<Cmd("service")>(Arg aArgs[])
+{
+    return ProcessService(aArgs, otDnsClientResolveService);
+}
+
+/**
+ * @cli dns servicehost
+ * @cparam dns servicehost @ca{service-instance-label} @ca{service-name} <!--
+ * -->                 [@ca{DNS-server-IP}] [@ca{DNS-server-port}] <!--
+ * -->                 [@ca{response-timeout-ms}] [@ca{max-tx-attempts}] <!--
+ * -->                 [@ca{recursion-desired-boolean}]
+ * @par api_copy
+ * #otDnsClientResolveServiceAndHostAddress
+ * @par
+ * Send a service instance resolution DNS query for a given service instance
+ * with potential follow-up host name resolution.
+ * Service instance label is provided first, followed by the service name
+ * (note that service instance label can contain dot '.' character).
+ * @par
+ * The parameters after `service-name` are optional. Any unspecified (or zero)
+ * value for these optional parameters is replaced by the value from the
+ * current default config (`dns config`).
+ * @par
+ * Note: The DNS server IP can be an IPv4 address, which will be synthesized
+ * to an IPv6 address using the preferred NAT64 prefix from the network data.
+ * The command will return `InvalidState` when the DNS server IP is an IPv4
+ * address but the preferred NAT64 prefix is unavailable.
+ * `OPENTHREAD_CONFIG_DNS_CLIENT_SERVICE_DISCOVERY_ENABLE` is required.
+ */
+template <> otError Dns::Process<Cmd("servicehost")>(Arg aArgs[])
+{
+    return ProcessService(aArgs, otDnsClientResolveServiceAndHostAddress);
+}
+
+otError Dns::ProcessService(Arg aArgs[], ResolveServiceFn aResolveServiceFn)
+{
+    otError           error = OT_ERROR_NONE;
+    otDnsQueryConfig  queryConfig;
+    otDnsQueryConfig *config = &queryConfig;
+
+    VerifyOrExit(!aArgs[1].IsEmpty(), error = OT_ERROR_INVALID_ARGS);
+    SuccessOrExit(error = GetDnsConfig(aArgs + 2, config));
+    SuccessOrExit(error = aResolveServiceFn(GetInstancePtr(), aArgs[0].GetCString(), aArgs[1].GetCString(),
+                                            &HandleDnsServiceResponse, this, config));
+    error = OT_ERROR_PENDING;
+
+exit:
+    return error;
+}
+
+#endif // OPENTHREAD_CONFIG_DNS_CLIENT_SERVICE_DISCOVERY_ENABLE
+
+//----------------------------------------------------------------------------------------------------------------------
+
+void Dns::OutputResult(otError aError) { Interpreter::GetInterpreter().OutputResult(aError); }
+
+otError Dns::GetDnsConfig(Arg aArgs[], otDnsQueryConfig *&aConfig)
+{
+    // This method gets the optional DNS config from `aArgs[]`.
+    // The format: `[server IP address] [server port] [timeout]
+    // [max tx attempt] [recursion desired] [service mode]
+    // [transport]`
+
+    otError error = OT_ERROR_NONE;
+    bool    recursionDesired;
+    bool    nat64SynthesizedAddress;
+
+    memset(aConfig, 0, sizeof(otDnsQueryConfig));
+
+    VerifyOrExit(!aArgs[0].IsEmpty(), aConfig = nullptr);
+
+    SuccessOrExit(error = Interpreter::ParseToIp6Address(GetInstancePtr(), aArgs[0], aConfig->mServerSockAddr.mAddress,
+                                                         nat64SynthesizedAddress));
+    if (nat64SynthesizedAddress)
+    {
+        OutputFormat("Synthesized IPv6 DNS server address: ");
+        OutputIp6AddressLine(aConfig->mServerSockAddr.mAddress);
+    }
+
+    VerifyOrExit(!aArgs[1].IsEmpty());
+    SuccessOrExit(error = aArgs[1].ParseAsUint16(aConfig->mServerSockAddr.mPort));
+
+    VerifyOrExit(!aArgs[2].IsEmpty());
+    SuccessOrExit(error = aArgs[2].ParseAsUint32(aConfig->mResponseTimeout));
+
+    VerifyOrExit(!aArgs[3].IsEmpty());
+    SuccessOrExit(error = aArgs[3].ParseAsUint8(aConfig->mMaxTxAttempts));
+
+    VerifyOrExit(!aArgs[4].IsEmpty());
+    SuccessOrExit(error = aArgs[4].ParseAsBool(recursionDesired));
+    aConfig->mRecursionFlag = recursionDesired ? OT_DNS_FLAG_RECURSION_DESIRED : OT_DNS_FLAG_NO_RECURSION;
+
+    VerifyOrExit(!aArgs[5].IsEmpty());
+    SuccessOrExit(error = ParseDnsServiceMode(aArgs[5], aConfig->mServiceMode));
+
+    VerifyOrExit(!aArgs[6].IsEmpty());
+
+    if (aArgs[6] == "tcp")
+    {
+        aConfig->mTransportProto = OT_DNS_TRANSPORT_TCP;
+    }
+    else if (aArgs[6] == "udp")
+    {
+        aConfig->mTransportProto = OT_DNS_TRANSPORT_UDP;
+    }
+    else
+    {
+        error = OT_ERROR_INVALID_ARGS;
+    }
+
+exit:
+    return error;
+}
+
+const char *const Dns::kServiceModeStrings[] = {
+    "unspec",      // OT_DNS_SERVICE_MODE_UNSPECIFIED      (0)
+    "srv",         // OT_DNS_SERVICE_MODE_SRV              (1)
+    "txt",         // OT_DNS_SERVICE_MODE_TXT              (2)
+    "srv_txt",     // OT_DNS_SERVICE_MODE_SRV_TXT          (3)
+    "srv_txt_sep", // OT_DNS_SERVICE_MODE_SRV_TXT_SEPARATE (4)
+    "srv_txt_opt", // OT_DNS_SERVICE_MODE_SRV_TXT_OPTIMIZE (5)
+};
+
+static_assert(OT_DNS_SERVICE_MODE_UNSPECIFIED == 0, "OT_DNS_SERVICE_MODE_UNSPECIFIED value is incorrect");
+static_assert(OT_DNS_SERVICE_MODE_SRV == 1, "OT_DNS_SERVICE_MODE_SRV value is incorrect");
+static_assert(OT_DNS_SERVICE_MODE_TXT == 2, "OT_DNS_SERVICE_MODE_TXT value is incorrect");
+static_assert(OT_DNS_SERVICE_MODE_SRV_TXT == 3, "OT_DNS_SERVICE_MODE_SRV_TXT value is incorrect");
+static_assert(OT_DNS_SERVICE_MODE_SRV_TXT_SEPARATE == 4, "OT_DNS_SERVICE_MODE_SRV_TXT_SEPARATE value is incorrect");
+static_assert(OT_DNS_SERVICE_MODE_SRV_TXT_OPTIMIZE == 5, "OT_DNS_SERVICE_MODE_SRV_TXT_OPTIMIZE value is incorrect");
+
+const char *Dns::DnsConfigServiceModeToString(otDnsServiceMode aMode) const
+{
+    return Stringify(aMode, kServiceModeStrings);
+}
+
+otError Dns::ParseDnsServiceMode(const Arg &aArg, otDnsServiceMode &aMode) const
+{
+    otError error = OT_ERROR_NONE;
+
+    if (aArg == "def")
+    {
+        aMode = OT_DNS_SERVICE_MODE_UNSPECIFIED;
+        ExitNow();
+    }
+
+    for (uint8_t index = 0; index < OT_ARRAY_LENGTH(kServiceModeStrings); index++)
+    {
+        if (aArg == kServiceModeStrings[index])
+        {
+            aMode = static_cast<otDnsServiceMode>(index);
+            ExitNow();
+        }
+    }
+
+    error = OT_ERROR_INVALID_ARGS;
+
+exit:
+    return error;
+}
+
+void Dns::HandleDnsAddressResponse(otError aError, const otDnsAddressResponse *aResponse, void *aContext)
+{
+    static_cast<Dns *>(aContext)->HandleDnsAddressResponse(aError, aResponse);
+}
+
+void Dns::HandleDnsAddressResponse(otError aError, const otDnsAddressResponse *aResponse)
+{
+    char         hostName[OT_DNS_MAX_NAME_SIZE];
+    otIp6Address address;
+    uint32_t     ttl;
+
+    IgnoreError(otDnsAddressResponseGetHostName(aResponse, hostName, sizeof(hostName)));
+
+    OutputFormat("DNS response for %s - ", hostName);
+
+    if (aError == OT_ERROR_NONE)
+    {
+        uint16_t index = 0;
+
+        while (otDnsAddressResponseGetAddress(aResponse, index, &address, &ttl) == OT_ERROR_NONE)
+        {
+            OutputIp6Address(address);
+            OutputFormat(" TTL:%lu ", ToUlong(ttl));
+            index++;
+        }
+    }
+
+    OutputNewLine();
+    OutputResult(aError);
+}
+
+#if OPENTHREAD_CONFIG_DNS_CLIENT_SERVICE_DISCOVERY_ENABLE
+
+void Dns::OutputDnsServiceInfo(uint8_t aIndentSize, const otDnsServiceInfo &aServiceInfo)
+{
+    OutputLine(aIndentSize, "Port:%d, Priority:%d, Weight:%d, TTL:%lu", aServiceInfo.mPort, aServiceInfo.mPriority,
+               aServiceInfo.mWeight, ToUlong(aServiceInfo.mTtl));
+    OutputLine(aIndentSize, "Host:%s", aServiceInfo.mHostNameBuffer);
+    OutputFormat(aIndentSize, "HostAddress:");
+    OutputIp6Address(aServiceInfo.mHostAddress);
+    OutputLine(" TTL:%lu", ToUlong(aServiceInfo.mHostAddressTtl));
+    OutputFormat(aIndentSize, "TXT:");
+
+    if (!aServiceInfo.mTxtDataTruncated)
+    {
+        OutputDnsTxtData(aServiceInfo.mTxtData, aServiceInfo.mTxtDataSize);
+    }
+    else
+    {
+        OutputFormat("[");
+        OutputBytes(aServiceInfo.mTxtData, aServiceInfo.mTxtDataSize);
+        OutputFormat("...]");
+    }
+
+    OutputLine(" TTL:%lu", ToUlong(aServiceInfo.mTxtDataTtl));
+}
+
+void Dns::HandleDnsBrowseResponse(otError aError, const otDnsBrowseResponse *aResponse, void *aContext)
+{
+    static_cast<Dns *>(aContext)->HandleDnsBrowseResponse(aError, aResponse);
+}
+
+void Dns::HandleDnsBrowseResponse(otError aError, const otDnsBrowseResponse *aResponse)
+{
+    char             name[OT_DNS_MAX_NAME_SIZE];
+    char             label[OT_DNS_MAX_LABEL_SIZE];
+    uint8_t          txtBuffer[kMaxTxtDataSize];
+    otDnsServiceInfo serviceInfo;
+
+    IgnoreError(otDnsBrowseResponseGetServiceName(aResponse, name, sizeof(name)));
+
+    OutputLine("DNS browse response for %s", name);
+
+    if (aError == OT_ERROR_NONE)
+    {
+        uint16_t index = 0;
+
+        while (otDnsBrowseResponseGetServiceInstance(aResponse, index, label, sizeof(label)) == OT_ERROR_NONE)
+        {
+            OutputLine("%s", label);
+            index++;
+
+            serviceInfo.mHostNameBuffer     = name;
+            serviceInfo.mHostNameBufferSize = sizeof(name);
+            serviceInfo.mTxtData            = txtBuffer;
+            serviceInfo.mTxtDataSize        = sizeof(txtBuffer);
+
+            if (otDnsBrowseResponseGetServiceInfo(aResponse, label, &serviceInfo) == OT_ERROR_NONE)
+            {
+                OutputDnsServiceInfo(kIndentSize, serviceInfo);
+            }
+
+            OutputNewLine();
+        }
+    }
+
+    OutputResult(aError);
+}
+
+void Dns::HandleDnsServiceResponse(otError aError, const otDnsServiceResponse *aResponse, void *aContext)
+{
+    static_cast<Dns *>(aContext)->HandleDnsServiceResponse(aError, aResponse);
+}
+
+void Dns::HandleDnsServiceResponse(otError aError, const otDnsServiceResponse *aResponse)
+{
+    char             name[OT_DNS_MAX_NAME_SIZE];
+    char             label[OT_DNS_MAX_LABEL_SIZE];
+    uint8_t          txtBuffer[kMaxTxtDataSize];
+    otDnsServiceInfo serviceInfo;
+
+    IgnoreError(otDnsServiceResponseGetServiceName(aResponse, label, sizeof(label), name, sizeof(name)));
+
+    OutputLine("DNS service resolution response for %s for service %s", label, name);
+
+    if (aError == OT_ERROR_NONE)
+    {
+        serviceInfo.mHostNameBuffer     = name;
+        serviceInfo.mHostNameBufferSize = sizeof(name);
+        serviceInfo.mTxtData            = txtBuffer;
+        serviceInfo.mTxtDataSize        = sizeof(txtBuffer);
+
+        if (otDnsServiceResponseGetServiceInfo(aResponse, &serviceInfo) == OT_ERROR_NONE)
+        {
+            OutputDnsServiceInfo(/* aIndentSize */ 0, serviceInfo);
+            OutputNewLine();
+        }
+    }
+
+    OutputResult(aError);
+}
+
+#endif // OPENTHREAD_CONFIG_DNS_CLIENT_SERVICE_DISCOVERY_ENABLE
+#endif // OPENTHREAD_CONFIG_DNS_CLIENT_ENABLE
+
+#if OPENTHREAD_CONFIG_DNSSD_SERVER_ENABLE
+
+template <> otError Dns::Process<Cmd("server")>(Arg aArgs[])
+{
+    otError error = OT_ERROR_NONE;
+
+    if (aArgs[0].IsEmpty())
+    {
+        error = OT_ERROR_INVALID_ARGS;
+    }
+#if OPENTHREAD_CONFIG_DNS_UPSTREAM_QUERY_ENABLE
+    else if (aArgs[0] == "upstream")
+    {
+        /**
+         * @cli dns server upstream
+         * @code
+         * dns server upstream
+         * Enabled
+         * Done
+         * @endcode
+         * @par api_copy
+         * #otDnssdUpstreamQueryIsEnabled
+         */
+        if (aArgs[1].IsEmpty())
+        {
+            OutputEnabledDisabledStatus(otDnssdUpstreamQueryIsEnabled(GetInstancePtr()));
+        }
+        /**
+         * @cli dns server upstream {enable|disable}
+         * @code
+         * dns server upstream enable
+         * Done
+         * @endcode
+         * @cparam dns server upstream @ca{enable|disable}
+         * @par api_copy
+         * #otDnssdUpstreamQuerySetEnabled
+         */
+        else
+        {
+            bool enable;
+
+            SuccessOrExit(error = Interpreter::ParseEnableOrDisable(aArgs[1], enable));
+            otDnssdUpstreamQuerySetEnabled(GetInstancePtr(), enable);
+        }
+    }
+#endif // OPENTHREAD_CONFIG_DNS_UPSTREAM_QUERY_ENABLE
+    else
+    {
+        ExitNow(error = OT_ERROR_INVALID_COMMAND);
+    }
+
+exit:
+    return error;
+}
+
+#endif // OPENTHREAD_CONFIG_DNSSD_SERVER_ENABLE
+
+otError Dns::Process(Arg aArgs[])
+{
+#define CmdEntry(aCommandString)                           \
+    {                                                      \
+        aCommandString, &Dns::Process<Cmd(aCommandString)> \
+    }
+
+    static constexpr Command kCommands[] = {
+
+#if OPENTHREAD_CONFIG_DNS_CLIENT_ENABLE && OPENTHREAD_CONFIG_DNS_CLIENT_SERVICE_DISCOVERY_ENABLE
+        CmdEntry("browse"),
+#endif
+#if OPENTHREAD_CONFIG_REFERENCE_DEVICE_ENABLE
+        CmdEntry("compression"),
+#endif
+#if OPENTHREAD_CONFIG_DNS_CLIENT_ENABLE
+        CmdEntry("config"),
+        CmdEntry("resolve"),
+#if OPENTHREAD_CONFIG_DNS_CLIENT_NAT64_ENABLE
+        CmdEntry("resolve4"),
+#endif
+#endif // OPENTHREAD_CONFIG_DNS_CLIENT_ENABLE
+#if OPENTHREAD_CONFIG_DNSSD_SERVER_ENABLE
+        CmdEntry("server"),
+#endif
+#if OPENTHREAD_CONFIG_DNS_CLIENT_ENABLE && OPENTHREAD_CONFIG_DNS_CLIENT_SERVICE_DISCOVERY_ENABLE
+        CmdEntry("service"),
+        CmdEntry("servicehost"),
+#endif
+    };
+
+#undef CmdEntry
+
+    static_assert(BinarySearch::IsSorted(kCommands), "kCommands is not sorted");
+
+    otError        error = OT_ERROR_INVALID_COMMAND;
+    const Command *command;
+
+    if (aArgs[0].IsEmpty() || (aArgs[0] == "help"))
+    {
+        OutputCommandTable(kCommands);
+        ExitNow(error = aArgs[0].IsEmpty() ? error : OT_ERROR_NONE);
+    }
+
+    command = BinarySearch::Find(aArgs[0].GetCString(), kCommands);
+    VerifyOrExit(command != nullptr);
+
+    error = (this->*command->mHandler)(aArgs + 1);
+
+exit:
+    return error;
+}
+
+} // namespace Cli
+} // namespace ot
+
+#endif // OPENTHREAD_CLI_DNS_ENABLE
diff --git a/src/cli/cli_dns.hpp b/src/cli/cli_dns.hpp
new file mode 100644
index 0000000..b9eb8d8
--- /dev/null
+++ b/src/cli/cli_dns.hpp
@@ -0,0 +1,131 @@
+/*
+ *  Copyright (c) 2023, The OpenThread Authors.
+ *  All rights reserved.
+ *
+ *  Redistribution and use in source and binary forms, with or without
+ *  modification, are permitted provided that the following conditions are met:
+ *  1. Redistributions of source code must retain the above copyright
+ *     notice, this list of conditions and the following disclaimer.
+ *  2. Redistributions in binary form must reproduce the above copyright
+ *     notice, this list of conditions and the following disclaimer in the
+ *     documentation and/or other materials provided with the distribution.
+ *  3. Neither the name of the copyright holder nor the
+ *     names of its contributors may be used to endorse or promote products
+ *     derived from this software without specific prior written permission.
+ *
+ *  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ *  AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ *  IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ *  ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ *  LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ *  CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ *  SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ *  INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ *  CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ *  ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ *  POSSIBILITY OF SUCH DAMAGE.
+ */
+
+/**
+ * @file
+ *   This file contains definitions for CLI to DNS (client and resolver).
+ */
+
+#ifndef CLI_DNS_HPP_
+#define CLI_DNS_HPP_
+
+#include "openthread-core-config.h"
+
+#ifdef OPENTHREAD_CLI_DNS_ENABLE
+#error "OPENTHREAD_CLI_DNS_ENABLE MUST not be defined directly, it is derived from `OPENTHREAD_CONFIG_*` configs".
+#endif
+
+#if OPENTHREAD_CONFIG_DNS_CLIENT_ENABLE || OPENTHREAD_CONFIG_DNSSD_SERVER_ENABLE || \
+    OPENTHREAD_CONFIG_REFERENCE_DEVICE_ENABLE
+#define OPENTHREAD_CLI_DNS_ENABLE 1
+#else
+#define OPENTHREAD_CLI_DNS_ENABLE 0
+#endif
+
+#if OPENTHREAD_CLI_DNS_ENABLE
+
+#include <openthread/dns.h>
+#include <openthread/dns_client.h>
+#include <openthread/dnssd_server.h>
+
+#include "cli/cli_config.h"
+#include "cli/cli_output.hpp"
+
+namespace ot {
+namespace Cli {
+
+/**
+ * This class implements the DNS CLI interpreter.
+ *
+ */
+class Dns : private Output
+{
+public:
+    typedef Utils::CmdLineParser::Arg Arg;
+
+    /**
+     * Constructor.
+     *
+     * @param[in]  aInstance            The OpenThread Instance.
+     * @param[in]  aOutputImplementer   An `OutputImplementer`.
+     *
+     */
+    Dns(otInstance *aInstance, OutputImplementer &aOutputImplementer)
+        : Output(aInstance, aOutputImplementer)
+    {
+    }
+
+    /**
+     * This method interprets a list of CLI arguments.
+     *
+     * @param[in]  aArgs        A pointer an array of command line arguments.
+     *
+     */
+    otError Process(Arg aArgs[]);
+
+private:
+    static constexpr uint8_t  kIndentSize     = 4;
+    static constexpr uint16_t kMaxTxtDataSize = OPENTHREAD_CONFIG_CLI_TXT_RECORD_MAX_SIZE;
+
+    using Command = CommandEntry<Dns>;
+
+    template <CommandId kCommandId> otError Process(Arg aArgs[]);
+
+#if OPENTHREAD_CONFIG_DNS_CLIENT_ENABLE
+    void        OutputResult(otError aError);
+    otError     GetDnsConfig(Arg aArgs[], otDnsQueryConfig *&aConfig);
+    const char *DnsConfigServiceModeToString(otDnsServiceMode aMode) const;
+    otError     ParseDnsServiceMode(const Arg &aArg, otDnsServiceMode &aMode) const;
+    static void HandleDnsAddressResponse(otError aError, const otDnsAddressResponse *aResponse, void *aContext);
+    void        HandleDnsAddressResponse(otError aError, const otDnsAddressResponse *aResponse);
+#if OPENTHREAD_CONFIG_DNS_CLIENT_SERVICE_DISCOVERY_ENABLE
+    static const char *const kServiceModeStrings[];
+
+    typedef otError (&ResolveServiceFn)(otInstance *,
+                                        const char *,
+                                        const char *,
+                                        otDnsServiceCallback,
+                                        void *,
+                                        const otDnsQueryConfig *);
+
+    otError     ProcessService(Arg aArgs[], ResolveServiceFn aResolveServiceFn);
+    void        OutputDnsServiceInfo(uint8_t aIndentSize, const otDnsServiceInfo &aServiceInfo);
+    static void HandleDnsBrowseResponse(otError aError, const otDnsBrowseResponse *aResponse, void *aContext);
+    void        HandleDnsBrowseResponse(otError aError, const otDnsBrowseResponse *aResponse);
+    static void HandleDnsServiceResponse(otError aError, const otDnsServiceResponse *aResponse, void *aContext);
+    void        HandleDnsServiceResponse(otError aError, const otDnsServiceResponse *aResponse);
+#endif
+#endif
+};
+
+} // namespace Cli
+} // namespace ot
+
+#endif // OPENTHREAD_CLI_DNS_ENABLE
+
+#endif // CLI_DNS_HPP_
diff --git a/src/cli/cli_extension_example.c b/src/cli/cli_extension_example.c
new file mode 100644
index 0000000..95d890a
--- /dev/null
+++ b/src/cli/cli_extension_example.c
@@ -0,0 +1,57 @@
+/*
+ *  Copyright (c) 2023, The OpenThread Authors.
+ *  All rights reserved.
+ *
+ *  Redistribution and use in source and binary forms, with or without
+ *  modification, are permitted provided that the following conditions are met:
+ *  1. Redistributions of source code must retain the above copyright
+ *     notice, this list of conditions and the following disclaimer.
+ *  2. Redistributions in binary form must reproduce the above copyright
+ *     notice, this list of conditions and the following disclaimer in the
+ *     documentation and/or other materials provided with the distribution.
+ *  3. Neither the name of the copyright holder nor the
+ *     names of its contributors may be used to endorse or promote products
+ *     derived from this software without specific prior written permission.
+ *
+ *  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ *  AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ *  IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ *  ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ *  LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ *  CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ *  SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ *  INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ *  CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ *  ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ *  POSSIBILITY OF SUCH DAMAGE.
+ */
+
+/*
+ * @file
+ * @brief # This file provides an example on how to implement a CLI vendor extension.
+ */
+
+#include <openthread/cli.h>
+#include "common/code_utils.hpp"
+
+static otError helloWorldCommand(void *aContext, uint8_t aArgsLength, char *aArgs[])
+{
+    otCliOutputFormat("Hello world!\r\n");
+    return OT_ERROR_NONE;
+}
+
+static otError threadCommand(void *aContext, uint8_t aArgsLength, char *aArgs[])
+{
+    otCliOutputFormat("Thread is great!\r\n");
+    return OT_ERROR_NONE;
+}
+
+static const otCliCommand sExtensionCommands[] = {
+    {"extensionhello", helloWorldCommand},
+    {"extensionthread", threadCommand},
+};
+
+void otCliVendorSetUserCommands(void)
+{
+    IgnoreError(otCliSetUserCommands(sExtensionCommands, OT_ARRAY_LENGTH(sExtensionCommands), NULL));
+}
diff --git a/tests/scripts/Makefile.am b/src/cli/cli_extension_example.cmake
similarity index 71%
rename from tests/scripts/Makefile.am
rename to src/cli/cli_extension_example.cmake
index d1cd8f1..94433f8 100644
--- a/tests/scripts/Makefile.am
+++ b/src/cli/cli_extension_example.cmake
@@ -1,5 +1,5 @@
 #
-#  Copyright (c) 2016-2017, The OpenThread Authors.
+#  Copyright (c) 2023, The OpenThread Authors.
 #  All rights reserved.
 #
 #  Redistribution and use in source and binary forms, with or without
@@ -26,26 +26,14 @@
 #  POSSIBILITY OF SUCH DAMAGE.
 #
 
-include $(abs_top_nlbuild_autotools_dir)/automake/pre.am
+# This file provides an example on how to implement a CLI vendor extension.
 
-# Always package (e.g. for 'make dist') these subdirectories.
+target_compile_definitions(ot-config INTERFACE "OPENTHREAD_CONFIG_CLI_MAX_USER_CMD_ENTRIES=2")
 
-DIST_SUBDIRS                            = \
-    thread-cert                           \
-    $(NULL)
+add_library(cli-extension-example cli_extension_example.c)
 
-# Always build (e.g. for 'make all') these subdirectories.
+target_link_libraries(cli-extension-example PRIVATE ot-config)
 
-SUBDIRS                                 = \
-    $(NULL)
+target_include_directories(cli-extension-example PUBLIC ${OT_PUBLIC_INCLUDES} PRIVATE ${COMMON_INCLUDES})
 
-if OPENTHREAD_POSIX
-if OPENTHREAD_ENABLE_CLI
-SUBDIRS                                += \
-    thread-cert                           \
-    $(NULL)
-endif
-endif
-
-include $(abs_top_nlbuild_autotools_dir)/automake/post.am
-
+set(OT_CLI_VENDOR_TARGET cli-extension-example)
diff --git a/src/cli/cli_history.cpp b/src/cli/cli_history.cpp
index e528565..e2fa087 100644
--- a/src/cli/cli_history.cpp
+++ b/src/cli/cli_history.cpp
@@ -43,7 +43,7 @@
 namespace Cli {
 
 static const char *const kSimpleEventStrings[] = {
-    "Added",  // (0) OT_HISTORY_TRACKER_{NET_DATA_ENTRY/ADDRESSS_EVENT}_ADDED
+    "Added",  // (0) OT_HISTORY_TRACKER_{NET_DATA_ENTRY/ADDRESS_EVENT}_ADDED
     "Removed" // (1) OT_HISTORY_TRACKER_{NET_DATA_ENTRY/ADDRESS_EVENT}_REMOVED
 };
 
@@ -572,7 +572,7 @@
     otHistoryTrackerEntryAgeToString(aEntryAge, ageString, sizeof(ageString));
 
     OutputLine("%s", ageString);
-    OutputFormat(kIndentSize, "type:%s len:%u cheksum:0x%04x sec:%s prio:%s ", MessageTypeToString(aInfo),
+    OutputFormat(kIndentSize, "type:%s len:%u checksum:0x%04x sec:%s prio:%s ", MessageTypeToString(aInfo),
                  aInfo.mPayloadLength, aInfo.mChecksum, aInfo.mLinkSecurity ? "yes" : "no",
                  MessagePriorityToString(aInfo.mPriority));
     if (aIsRx)
diff --git a/src/cli/cli_mac_filter.cpp b/src/cli/cli_mac_filter.cpp
new file mode 100644
index 0000000..6525a20
--- /dev/null
+++ b/src/cli/cli_mac_filter.cpp
@@ -0,0 +1,297 @@
+/*
+ *  Copyright (c) 2023, The OpenThread Authors.
+ *  All rights reserved.
+ *
+ *  Redistribution and use in source and binary forms, with or without
+ *  modification, are permitted provided that the following conditions are met:
+ *  1. Redistributions of source code must retain the above copyright
+ *     notice, this list of conditions and the following disclaimer.
+ *  2. Redistributions in binary form must reproduce the above copyright
+ *     notice, this list of conditions and the following disclaimer in the
+ *     documentation and/or other materials provided with the distribution.
+ *  3. Neither the name of the copyright holder nor the
+ *     names of its contributors may be used to endorse or promote products
+ *     derived from this software without specific prior written permission.
+ *
+ *  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ *  AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ *  IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ *  ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ *  LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ *  CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ *  SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ *  INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ *  CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ *  ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ *  POSSIBILITY OF SUCH DAMAGE.
+ */
+
+/**
+ * @file
+ *   This file implements CLI for MAC Filter.
+ */
+
+#include "cli_mac_filter.hpp"
+
+#include "cli/cli.hpp"
+
+#if OPENTHREAD_CONFIG_MAC_FILTER_ENABLE
+
+namespace ot {
+namespace Cli {
+
+void MacFilter::OutputFilter(uint8_t aFilters)
+{
+    otMacFilterEntry    entry;
+    otMacFilterIterator iterator;
+
+    if (aFilters & kAddressFilter)
+    {
+        if ((aFilters & ~kAddressFilter) != 0)
+        {
+            OutputFormat("Address Mode: ");
+        }
+
+        OutputLine("%s", AddressModeToString(otLinkFilterGetAddressMode(GetInstancePtr())));
+
+        iterator = OT_MAC_FILTER_ITERATOR_INIT;
+
+        while (otLinkFilterGetNextAddress(GetInstancePtr(), &iterator, &entry) == OT_ERROR_NONE)
+        {
+            OutputEntry(entry);
+        }
+    }
+
+    if (aFilters & kRssFilter)
+    {
+        if ((aFilters & ~kRssFilter) != 0)
+        {
+            OutputLine("RssIn List:");
+        }
+
+        iterator = OT_MAC_FILTER_ITERATOR_INIT;
+
+        while (otLinkFilterGetNextRssIn(GetInstancePtr(), &iterator, &entry) == OT_ERROR_NONE)
+        {
+            if (IsDefaultRss(entry.mExtAddress))
+            {
+                OutputLine("Default rss: %d (lqi %u)", entry.mRssIn,
+                           otLinkConvertRssToLinkQuality(GetInstancePtr(), entry.mRssIn));
+            }
+            else
+            {
+                OutputEntry(entry);
+            }
+        }
+    }
+}
+
+bool MacFilter::IsDefaultRss(const otExtAddress &aExtAddress)
+{
+    // In default RSS entry, the extended address will be all `0xff`.
+
+    bool isDefault = true;
+
+    for (uint8_t byte : aExtAddress.m8)
+    {
+        if (byte != 0xff)
+        {
+            isDefault = false;
+            break;
+        }
+    }
+
+    return isDefault;
+}
+
+const char *MacFilter::AddressModeToString(otMacFilterAddressMode aMode)
+{
+    static const char *const kModeStrings[] = {
+        "Disabled",  // (0) OT_MAC_FILTER_ADDRESS_MODE_DISABLED
+        "Allowlist", // (1) OT_MAC_FILTER_ADDRESS_MODE_ALLOWLIST
+        "Denylist",  // (2) OT_MAC_FILTER_ADDRESS_MODE_DENYLIST
+    };
+
+    static_assert(0 == OT_MAC_FILTER_ADDRESS_MODE_DISABLED, "OT_MAC_FILTER_ADDRESS_MODE_DISABLED value is incorrect");
+    static_assert(1 == OT_MAC_FILTER_ADDRESS_MODE_ALLOWLIST, "OT_MAC_FILTER_ADDRESS_MODE_ALLOWLIST value is incorrect");
+    static_assert(2 == OT_MAC_FILTER_ADDRESS_MODE_DENYLIST, "OT_MAC_FILTER_ADDRESS_MODE_DENYLIST value is incorrect");
+
+    return Stringify(aMode, kModeStrings);
+}
+
+void MacFilter::OutputEntry(const otMacFilterEntry &aEntry)
+{
+    OutputExtAddress(aEntry.mExtAddress);
+
+    if (aEntry.mRssIn != OT_MAC_FILTER_FIXED_RSS_DISABLED)
+    {
+        OutputFormat(" : rss %d (lqi %d)", aEntry.mRssIn,
+                     otLinkConvertRssToLinkQuality(GetInstancePtr(), aEntry.mRssIn));
+    }
+
+    OutputNewLine();
+}
+
+template <> otError MacFilter::Process<Cmd("addr")>(Arg aArgs[])
+{
+    otError      error = OT_ERROR_NONE;
+    otExtAddress extAddr;
+
+    if (aArgs[0].IsEmpty())
+    {
+        OutputFilter(kAddressFilter);
+    }
+    else if (aArgs[0] == "add")
+    {
+        SuccessOrExit(error = aArgs[1].ParseAsHexString(extAddr.m8));
+        error = otLinkFilterAddAddress(GetInstancePtr(), &extAddr);
+
+        VerifyOrExit(error == OT_ERROR_NONE || error == OT_ERROR_ALREADY);
+
+        if (!aArgs[2].IsEmpty())
+        {
+            int8_t rss;
+
+            SuccessOrExit(error = aArgs[2].ParseAsInt8(rss));
+            SuccessOrExit(error = otLinkFilterAddRssIn(GetInstancePtr(), &extAddr, rss));
+        }
+    }
+    else if (aArgs[0] == "remove")
+    {
+        SuccessOrExit(error = aArgs[1].ParseAsHexString(extAddr.m8));
+        otLinkFilterRemoveAddress(GetInstancePtr(), &extAddr);
+    }
+    else if (aArgs[0] == "clear")
+    {
+        otLinkFilterClearAddresses(GetInstancePtr());
+    }
+    else
+    {
+        static const char *const kModeCommands[] = {
+            "disable",   // (0) OT_MAC_FILTER_ADDRESS_MODE_DISABLED
+            "allowlist", // (1) OT_MAC_FILTER_ADDRESS_MODE_ALLOWLIST
+            "denylist",  // (2) OT_MAC_FILTER_ADDRESS_MODE_DENYLIST
+        };
+
+        for (uint8_t index = 0; index < OT_ARRAY_LENGTH(kModeCommands); index++)
+        {
+            if (aArgs[0] == kModeCommands[index])
+            {
+                VerifyOrExit(aArgs[1].IsEmpty(), error = OT_ERROR_INVALID_ARGS);
+                otLinkFilterSetAddressMode(GetInstancePtr(), static_cast<otMacFilterAddressMode>(index));
+                ExitNow();
+            }
+        }
+
+        error = OT_ERROR_INVALID_COMMAND;
+    }
+
+exit:
+    return error;
+}
+
+template <> otError MacFilter::Process<Cmd("rss")>(Arg aArgs[])
+{
+    otError      error = OT_ERROR_NONE;
+    otExtAddress extAddr;
+    int8_t       rss;
+
+    if (aArgs[0].IsEmpty())
+    {
+        OutputFilter(kRssFilter);
+    }
+    else if (aArgs[0] == "add-lqi")
+    {
+        uint8_t linkQuality;
+
+        SuccessOrExit(error = aArgs[2].ParseAsUint8(linkQuality));
+        VerifyOrExit(linkQuality <= 3, error = OT_ERROR_INVALID_ARGS);
+        rss = otLinkConvertLinkQualityToRss(GetInstancePtr(), linkQuality);
+
+        if (aArgs[1] == "*")
+        {
+            otLinkFilterSetDefaultRssIn(GetInstancePtr(), rss);
+        }
+        else
+        {
+            SuccessOrExit(error = aArgs[1].ParseAsHexString(extAddr.m8));
+            error = otLinkFilterAddRssIn(GetInstancePtr(), &extAddr, rss);
+        }
+    }
+    else if (aArgs[0] == "add")
+    {
+        SuccessOrExit(error = aArgs[2].ParseAsInt8(rss));
+
+        if (aArgs[1] == "*")
+        {
+            otLinkFilterSetDefaultRssIn(GetInstancePtr(), rss);
+        }
+        else
+        {
+            SuccessOrExit(error = aArgs[1].ParseAsHexString(extAddr.m8));
+            error = otLinkFilterAddRssIn(GetInstancePtr(), &extAddr, rss);
+        }
+    }
+    else if (aArgs[0] == "remove")
+    {
+        if (aArgs[1] == "*")
+        {
+            otLinkFilterClearDefaultRssIn(GetInstancePtr());
+        }
+        else
+        {
+            SuccessOrExit(error = aArgs[1].ParseAsHexString(extAddr.m8));
+            otLinkFilterRemoveRssIn(GetInstancePtr(), &extAddr);
+        }
+    }
+    else if (aArgs[0] == "clear")
+    {
+        otLinkFilterClearAllRssIn(GetInstancePtr());
+    }
+    else
+    {
+        error = OT_ERROR_INVALID_COMMAND;
+    }
+
+exit:
+    return error;
+}
+
+otError MacFilter::Process(Arg aArgs[])
+{
+#define CmdEntry(aCommandString)                                 \
+    {                                                            \
+        aCommandString, &MacFilter::Process<Cmd(aCommandString)> \
+    }
+
+    static constexpr Command kCommands[] = {
+        CmdEntry("addr"),
+        CmdEntry("rss"),
+    };
+
+#undef CmdEntry
+
+    static_assert(BinarySearch::IsSorted(kCommands), "kCommands is not sorted");
+
+    otError        error = OT_ERROR_INVALID_COMMAND;
+    const Command *command;
+
+    if (aArgs[0].IsEmpty())
+    {
+        OutputFilter(kAddressFilter | kRssFilter);
+        ExitNow(error = OT_ERROR_NONE);
+    }
+
+    command = BinarySearch::Find(aArgs[0].GetCString(), kCommands);
+    VerifyOrExit(command != nullptr);
+
+    error = (this->*command->mHandler)(aArgs + 1);
+
+exit:
+    return error;
+}
+
+} // namespace Cli
+} // namespace ot
+
+#endif // OPENTHREAD_CONFIG_MAC_FILTER_ENABLE
diff --git a/src/cli/cli_mac_filter.hpp b/src/cli/cli_mac_filter.hpp
new file mode 100644
index 0000000..da2c898
--- /dev/null
+++ b/src/cli/cli_mac_filter.hpp
@@ -0,0 +1,105 @@
+/*
+ *  Copyright (c) 2023, The OpenThread Authors.
+ *  All rights reserved.
+ *
+ *  Redistribution and use in source and binary forms, with or without
+ *  modification, are permitted provided that the following conditions are met:
+ *  1. Redistributions of source code must retain the above copyright
+ *     notice, this list of conditions and the following disclaimer.
+ *  2. Redistributions in binary form must reproduce the above copyright
+ *     notice, this list of conditions and the following disclaimer in the
+ *     documentation and/or other materials provided with the distribution.
+ *  3. Neither the name of the copyright holder nor the
+ *     names of its contributors may be used to endorse or promote products
+ *     derived from this software without specific prior written permission.
+ *
+ *  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ *  AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ *  IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ *  ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ *  LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ *  CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ *  SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ *  INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ *  CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ *  ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ *  POSSIBILITY OF SUCH DAMAGE.
+ */
+
+/**
+ * @file
+ *   This file contains definitions for CLI to MAC Filter.
+ */
+
+#ifndef CLI_MAC_FILTER_HPP_
+#define CLI_MAC_FILTER_HPP_
+
+#include "openthread-core-config.h"
+
+#if OPENTHREAD_CONFIG_MAC_FILTER_ENABLE
+
+#include <openthread/link.h>
+
+#include "cli/cli_config.h"
+#include "cli/cli_output.hpp"
+
+namespace ot {
+namespace Cli {
+
+/**
+ * This class implements the MAC Filter CLI interpreter.
+ *
+ */
+class MacFilter : private Output
+{
+public:
+    typedef Utils::CmdLineParser::Arg Arg;
+
+    /**
+     * Constructor.
+     *
+     * @param[in]  aInstance            The OpenThread Instance.
+     * @param[in]  aOutputImplementer   An `OutputImplementer`.
+     *
+     */
+    MacFilter(otInstance *aInstance, OutputImplementer &aOutputImplementer)
+        : Output(aInstance, aOutputImplementer)
+    {
+    }
+
+    /**
+     * This method interprets a list of CLI arguments.
+     *
+     * @param[in]  aArgs        A pointer an array of command line arguments.
+     *
+     * @reval  OT_ERROR_NONE              Successfully executed the CLI command.
+     * @retval OT_ERROR_INVALID_COMMAND   Invalid or unknown CLI command.
+     * @retavl OT_ERROR_INVALID_ARGS      Invalid arguments.
+     * @retval ...                        Error handling the command.
+     *
+     */
+    otError Process(Arg aArgs[]);
+
+private:
+    static constexpr uint8_t kIndentSize = 4;
+
+    using Command = CommandEntry<MacFilter>;
+
+    template <CommandId kCommandId> otError Process(Arg aArgs[]);
+
+    // For use as input to  `OutputFilter()`
+    static constexpr uint8_t kAddressFilter = (1U << 0);
+    static constexpr uint8_t kRssFilter     = (1U << 1);
+
+    void               OutputFilter(uint8_t aFilters);
+    void               OutputEntry(const otMacFilterEntry &aEntry);
+    static bool        IsDefaultRss(const otExtAddress &aExtAddress);
+    static const char *AddressModeToString(otMacFilterAddressMode aMode);
+};
+
+} // namespace Cli
+} // namespace ot
+
+#endif // OPENTHREAD_CLI_DNS_ENABLE
+
+#endif // CLI_MAC_FILTER_HPP_
diff --git a/src/cli/cli_network_data.cpp b/src/cli/cli_network_data.cpp
index 499a5fb..36fcf3d 100644
--- a/src/cli/cli_network_data.cpp
+++ b/src/cli/cli_network_data.cpp
@@ -354,6 +354,29 @@
         error = otNetDataPublishExternalRoute(GetInstancePtr(), &config);
         ExitNow();
     }
+
+    /**
+     * @cli netdata publish replace
+     * @code
+     * netdata publish replace ::/0 fd00:1234:5678::/64 s high
+     * Done
+     * @endcode
+     * @cparam netdata publish replace @ca{oldprefix} @ca{prefix} [@ca{sn}] [@ca{high}|@ca{med}|@ca{low}]
+     * OT CLI uses mapped arguments to configure #otExternalRouteConfig values. @moreinfo{the @overview}.
+     * @par
+     * Replaces a previously published external route entry. @moreinfo{@netdata}.
+     * @sa otNetDataReplacePublishedExternalRoute
+     */
+    if (aArgs[0] == "replace")
+    {
+        otIp6Prefix           prefix;
+        otExternalRouteConfig config;
+
+        SuccessOrExit(error = aArgs[1].ParseAsIp6Prefix(prefix));
+        SuccessOrExit(error = Interpreter::ParseRoute(aArgs + 2, config));
+        error = otNetDataReplacePublishedExternalRoute(GetInstancePtr(), &prefix, &config);
+        ExitNow();
+    }
 #endif // OPENTHREAD_CONFIG_BORDER_ROUTER_ENABLE
 
     error = OT_ERROR_INVALID_ARGS;
diff --git a/src/cli/cli_udp.cpp b/src/cli/cli_udp.cpp
index 6edc257..d5f3792 100644
--- a/src/cli/cli_udp.cpp
+++ b/src/cli/cli_udp.cpp
@@ -169,7 +169,7 @@
         // Binary hex data payload
 
         VerifyOrExit(!aArgs[1].IsEmpty(), error = OT_ERROR_INVALID_ARGS);
-        SuccessOrExit(error = PrepareHexStringPaylod(*message, aArgs[1].GetCString()));
+        SuccessOrExit(error = PrepareHexStringPayload(*message, aArgs[1].GetCString()));
     }
     else
     {
@@ -243,7 +243,7 @@
     return error;
 }
 
-otError UdpExample::PrepareHexStringPaylod(otMessage &aMessage, const char *aHexString)
+otError UdpExample::PrepareHexStringPayload(otMessage &aMessage, const char *aHexString)
 {
     enum : uint8_t
     {
diff --git a/src/cli/cli_udp.hpp b/src/cli/cli_udp.hpp
index 4bfb9ea..55996c7 100644
--- a/src/cli/cli_udp.hpp
+++ b/src/cli/cli_udp.hpp
@@ -75,7 +75,7 @@
     template <CommandId kCommandId> otError Process(Arg aArgs[]);
 
     static otError PrepareAutoGeneratedPayload(otMessage &aMessage, uint16_t aPayloadLength);
-    static otError PrepareHexStringPaylod(otMessage &aMessage, const char *aHexString);
+    static otError PrepareHexStringPayload(otMessage &aMessage, const char *aHexString);
 
     static void HandleUdpReceive(void *aContext, otMessage *aMessage, const otMessageInfo *aMessageInfo);
     void        HandleUdpReceive(otMessage *aMessage, const otMessageInfo *aMessageInfo);
diff --git a/src/cli/ftd.cmake b/src/cli/ftd.cmake
index 22f1e11..df9b37a 100644
--- a/src/cli/ftd.cmake
+++ b/src/cli/ftd.cmake
@@ -49,3 +49,7 @@
         ot-config-ftd
         ot-config
 )
+
+if(OT_CLI_VENDOR_TARGET)
+    target_link_libraries(openthread-cli-ftd PRIVATE ${OT_CLI_VENDOR_TARGET})
+endif()
diff --git a/src/cli/mtd.cmake b/src/cli/mtd.cmake
index 3a875c3..b5eb1db 100644
--- a/src/cli/mtd.cmake
+++ b/src/cli/mtd.cmake
@@ -49,3 +49,7 @@
         ot-config-mtd
         ot-config
 )
+
+if(OT_CLI_VENDOR_TARGET)
+    target_link_libraries(openthread-cli-mtd PRIVATE ${OT_CLI_VENDOR_TARGET})
+endif()
diff --git a/src/cli/radio.cmake b/src/cli/radio.cmake
index 6fa0b91..28320d6 100644
--- a/src/cli/radio.cmake
+++ b/src/cli/radio.cmake
@@ -58,3 +58,7 @@
         ot-config-radio
         ot-config
 )
+
+if(OT_CLI_VENDOR_TARGET)
+    target_link_libraries(openthread-cli-radio PRIVATE ${OT_CLI_VENDOR_TARGET})
+endif()
diff --git a/src/core/BUILD.gn b/src/core/BUILD.gn
index 917f424..19190c0 100644
--- a/src/core/BUILD.gn
+++ b/src/core/BUILD.gn
@@ -196,18 +196,19 @@
       defines += [ "OPENTHREAD_CONFIG_MLE_LONG_ROUTES_ENABLE=1" ]
     }
 
-    if (openthread_config_tmf_network_diag_mtd_enable) {
-      defines += [ "OPENTHREAD_CONFIG_TMF_NETWORK_DIAG_MTD_ENABLE=1" ]
-    }
-
     if (openthread_config_multiple_instance_enable) {
       defines += [ "OPENTHREAD_CONFIG_MULTIPLE_INSTANCE_ENABLE=1" ]
     }
 
+    if (openthread_config_tmf_netdiag_client_enable) {
+      defines += [ "OPENTHREAD_CONFIG_TMF_NETDIAG_CLIENT_ENABLE=1" ]
+    }
+
     if (openthread_config_platform_netif_enable) {
       defines += [ "OPENTHREAD_CONFIG_PLATFORM_NETIF_ENABLE=1" ]
     }
 
+
     if (openthread_config_platform_udp_enable) {
       defines += [ "OPENTHREAD_CONFIG_PLATFORM_UDP_ENABLE=1" ]
     }
@@ -795,6 +796,7 @@
     "config/mle.h",
     "config/nat64.h",
     "config/netdata_publisher.h",
+    "config/network_diagnostic.h",
     "config/openthread-core-config-check.h",
     "config/parent_search.h",
     "config/ping_sender.h",
diff --git a/src/core/Makefile.am b/src/core/Makefile.am
index 169902e..92c2bf6 100644
--- a/src/core/Makefile.am
+++ b/src/core/Makefile.am
@@ -520,6 +520,7 @@
     config/mle.h                                  \
     config/nat64.h                                \
     config/netdata_publisher.h                    \
+    config/network_diagnostic.h                   \
     config/openthread-core-config-check.h         \
     config/parent_search.h                        \
     config/ping_sender.h                          \
diff --git a/src/core/api/border_agent_api.cpp b/src/core/api/border_agent_api.cpp
index a6ccea1..3590116 100644
--- a/src/core/api/border_agent_api.cpp
+++ b/src/core/api/border_agent_api.cpp
@@ -42,6 +42,18 @@
 
 using namespace ot;
 
+#if OPENTHREAD_CONFIG_BORDER_AGENT_ID_ENABLE
+otError otBorderAgentGetId(otInstance *aInstance, otBorderAgentId *aId)
+{
+    return AsCoreType(aInstance).Get<MeshCoP::BorderAgent>().GetId(AsCoreType(aId));
+}
+
+otError otBorderAgentSetId(otInstance *aInstance, const otBorderAgentId *aId)
+{
+    return AsCoreType(aInstance).Get<MeshCoP::BorderAgent>().SetId(AsCoreType(aId));
+}
+#endif
+
 otBorderAgentState otBorderAgentGetState(otInstance *aInstance)
 {
     return MapEnum(AsCoreType(aInstance).Get<MeshCoP::BorderAgent>().GetState());
diff --git a/src/core/api/dns_api.cpp b/src/core/api/dns_api.cpp
index f1020ec..80246ca 100644
--- a/src/core/api/dns_api.cpp
+++ b/src/core/api/dns_api.cpp
@@ -188,6 +188,20 @@
                                                                    AsCoreTypePtr(aConfig));
 }
 
+otError otDnsClientResolveServiceAndHostAddress(otInstance             *aInstance,
+                                                const char             *aInstanceLabel,
+                                                const char             *aServiceName,
+                                                otDnsServiceCallback    aCallback,
+                                                void                   *aContext,
+                                                const otDnsQueryConfig *aConfig)
+{
+    AssertPointerIsNotNull(aInstanceLabel);
+    AssertPointerIsNotNull(aServiceName);
+
+    return AsCoreType(aInstance).Get<Dns::Client>().ResolveServiceAndHostAddress(
+        aInstanceLabel, aServiceName, aCallback, aContext, AsCoreTypePtr(aConfig));
+}
+
 otError otDnsServiceResponseGetServiceName(const otDnsServiceResponse *aResponse,
                                            char                       *aLabelBuffer,
                                            uint8_t                     aLabelBufferSize,
diff --git a/src/core/api/instance_api.cpp b/src/core/api/instance_api.cpp
index 61c9835..ad2a89e 100644
--- a/src/core/api/instance_api.cpp
+++ b/src/core/api/instance_api.cpp
@@ -43,7 +43,7 @@
 
 #if !defined(OPENTHREAD_BUILD_DATETIME)
 #ifdef __ANDROID__
-#ifdef OPENTHREAD_ENABLE_ANDROID_NDK
+#ifdef OPENTHREAD_CONFIG_ANDROID_NDK_ENABLE
 #include <sys/system_properties.h>
 #else
 #include <cutils/properties.h>
@@ -70,6 +70,8 @@
 otInstance *otInstanceInitSingle(void) { return &Instance::InitSingle(); }
 #endif // #if OPENTHREAD_CONFIG_MULTIPLE_INSTANCE_ENABLE
 
+uint32_t otInstanceGetId(otInstance *aInstance) { return AsCoreType(aInstance).GetId(); }
+
 bool otInstanceIsInitialized(otInstance *aInstance)
 {
 #if OPENTHREAD_MTD || OPENTHREAD_FTD
@@ -135,7 +137,7 @@
 
 #if !defined(OPENTHREAD_BUILD_DATETIME) && defined(__ANDROID__)
 
-#ifdef OPENTHREAD_ENABLE_ANDROID_NDK
+#ifdef OPENTHREAD_CONFIG_ANDROID_NDK_ENABLE
     static char sVersion[100 + PROP_VALUE_MAX];
     char        dateTime[PROP_VALUE_MAX];
 
diff --git a/src/core/api/ip6_api.cpp b/src/core/api/ip6_api.cpp
index 18769f7..4ff7d22 100644
--- a/src/core/api/ip6_api.cpp
+++ b/src/core/api/ip6_api.cpp
@@ -146,9 +146,8 @@
                                      uint16_t                 aDataLength,
                                      const otMessageSettings *aSettings)
 {
-    return (aSettings != nullptr)
-               ? AsCoreType(aInstance).Get<Ip6::Ip6>().NewMessage(aData, aDataLength, AsCoreType(aSettings))
-               : AsCoreType(aInstance).Get<Ip6::Ip6>().NewMessage(aData, aDataLength);
+    return AsCoreType(aInstance).Get<Ip6::Ip6>().NewMessageFromData(aData, aDataLength,
+                                                                    Message::Settings::From(aSettings));
 }
 
 otError otIp6AddUnsecurePort(otInstance *aInstance, uint16_t aPort)
@@ -188,6 +187,11 @@
     return AsCoreType(aAddress).FromString(aString);
 }
 
+otError otIp6PrefixFromString(const char *aString, otIp6Prefix *aPrefix)
+{
+    return AsCoreType(aPrefix).FromString(aString);
+}
+
 void otIp6AddressToString(const otIp6Address *aAddress, char *aBuffer, uint16_t aSize)
 {
     AssertPointerIsNotNull(aBuffer);
diff --git a/src/core/api/link_metrics_api.cpp b/src/core/api/link_metrics_api.cpp
index 1ee6acb..548876d 100644
--- a/src/core/api/link_metrics_api.cpp
+++ b/src/core/api/link_metrics_api.cpp
@@ -33,8 +33,7 @@
 
 #include "openthread-core-config.h"
 
-#if OPENTHREAD_CONFIG_MLE_LINK_METRICS_INITIATOR_ENABLE || OPENTHREAD_CONFIG_MLE_LINK_METRICS_SUBJECT_ENABLE
-
+#if OPENTHREAD_CONFIG_MLE_LINK_METRICS_INITIATOR_ENABLE
 #include <openthread/link_metrics.h>
 
 #include "common/as_core_type.hpp"
@@ -49,10 +48,10 @@
                            otLinkMetricsReportCallback aCallback,
                            void                       *aCallbackContext)
 {
-    AsCoreType(aInstance).Get<LinkMetrics::LinkMetrics>().SetReportCallback(aCallback, aCallbackContext);
+    AsCoreType(aInstance).Get<LinkMetrics::Initiator>().SetReportCallback(aCallback, aCallbackContext);
 
-    return AsCoreType(aInstance).Get<LinkMetrics::LinkMetrics>().Query(AsCoreType(aDestination), aSeriesId,
-                                                                       AsCoreTypePtr(aLinkMetricsFlags));
+    return AsCoreType(aInstance).Get<LinkMetrics::Initiator>().Query(AsCoreType(aDestination), aSeriesId,
+                                                                     AsCoreTypePtr(aLinkMetricsFlags));
 }
 
 #if OPENTHREAD_CONFIG_MLE_LINK_METRICS_INITIATOR_ENABLE
@@ -64,12 +63,12 @@
                                                  otLinkMetricsMgmtResponseCallback aCallback,
                                                  void                             *aCallbackContext)
 {
-    LinkMetrics::LinkMetrics &linkMetrics = AsCoreType(aInstance).Get<LinkMetrics::LinkMetrics>();
+    LinkMetrics::Initiator &initiator = AsCoreType(aInstance).Get<LinkMetrics::Initiator>();
 
-    linkMetrics.SetMgmtResponseCallback(aCallback, aCallbackContext);
+    initiator.SetMgmtResponseCallback(aCallback, aCallbackContext);
 
-    return linkMetrics.SendMgmtRequestForwardTrackingSeries(
-        AsCoreType(aDestination), aSeriesId, AsCoreType(&aSeriesFlags), AsCoreTypePtr(aLinkMetricsFlags));
+    return initiator.SendMgmtRequestForwardTrackingSeries(AsCoreType(aDestination), aSeriesId,
+                                                          AsCoreType(&aSeriesFlags), AsCoreTypePtr(aLinkMetricsFlags));
 }
 
 otError otLinkMetricsConfigEnhAckProbing(otInstance                                *aInstance,
@@ -81,13 +80,13 @@
                                          otLinkMetricsEnhAckProbingIeReportCallback aEnhAckCallback,
                                          void                                      *aEnhAckCallbackContext)
 {
-    LinkMetrics::LinkMetrics &linkMetrics = AsCoreType(aInstance).Get<LinkMetrics::LinkMetrics>();
+    LinkMetrics::Initiator &initiator = AsCoreType(aInstance).Get<LinkMetrics::Initiator>();
 
-    linkMetrics.SetMgmtResponseCallback(aCallback, aCallbackContext);
-    linkMetrics.SetEnhAckProbingCallback(aEnhAckCallback, aEnhAckCallbackContext);
+    initiator.SetMgmtResponseCallback(aCallback, aCallbackContext);
+    initiator.SetEnhAckProbingCallback(aEnhAckCallback, aEnhAckCallbackContext);
 
-    return linkMetrics.SendMgmtRequestEnhAckProbing(AsCoreType(aDestination), MapEnum(aEnhAckFlags),
-                                                    AsCoreTypePtr(aLinkMetricsFlags));
+    return initiator.SendMgmtRequestEnhAckProbing(AsCoreType(aDestination), MapEnum(aEnhAckFlags),
+                                                  AsCoreTypePtr(aLinkMetricsFlags));
 }
 
 otError otLinkMetricsSendLinkProbe(otInstance         *aInstance,
@@ -95,10 +94,10 @@
                                    uint8_t             aSeriesId,
                                    uint8_t             aLength)
 {
-    LinkMetrics::LinkMetrics &linkMetrics = AsCoreType(aInstance).Get<LinkMetrics::LinkMetrics>();
+    LinkMetrics::Initiator &initiator = AsCoreType(aInstance).Get<LinkMetrics::Initiator>();
 
-    return linkMetrics.SendLinkProbe(AsCoreType(aDestination), aSeriesId, aLength);
+    return initiator.SendLinkProbe(AsCoreType(aDestination), aSeriesId, aLength);
 }
 #endif
 
-#endif // OPENTHREAD_CONFIG_MLE_LINK_METRICS_INITIATOR_ENABLE || OPENTHREAD_CONFIG_MLE_LINK_METRICS_SUBJECT_ENABLE
+#endif // OPENTHREAD_CONFIG_MLE_LINK_METRICS_INITIATOR_ENABLE
diff --git a/src/core/api/nat64_api.cpp b/src/core/api/nat64_api.cpp
index e296a47..7405b9d 100644
--- a/src/core/api/nat64_api.cpp
+++ b/src/core/api/nat64_api.cpp
@@ -46,7 +46,7 @@
 
 using namespace ot;
 
-// Note: We support the following scenrios:
+// Note: We support the following scenarios:
 // - Using OpenThread's routing manager, while using external NAT64 translator (like tayga).
 // - Using OpenThread's NAT64 translator, while using external routing manager.
 // So OPENTHREAD_CONFIG_NAT64_TRANSLATOR_ENABLE translator and OPENTHREAD_CONFIG_NAT64_BORDER_ROUTING_ENABLE are two
@@ -166,6 +166,8 @@
     AsCoreType(aAddress).ToString(aBuffer, aSize);
 }
 
+otError otIp4CidrFromString(const char *aString, otIp4Cidr *aCidr) { return AsCoreType(aCidr).FromString(aString); }
+
 void otIp4CidrToString(const otIp4Cidr *aCidr, char *aBuffer, uint16_t aSize)
 {
     AssertPointerIsNotNull(aBuffer);
diff --git a/src/core/api/netdata_publisher_api.cpp b/src/core/api/netdata_publisher_api.cpp
index 62b48f5..9101b0a 100644
--- a/src/core/api/netdata_publisher_api.cpp
+++ b/src/core/api/netdata_publisher_api.cpp
@@ -92,6 +92,14 @@
                                                                                     NetworkData::Publisher::kFromUser);
 }
 
+otError otNetDataReplacePublishedExternalRoute(otInstance                  *aInstance,
+                                               const otIp6Prefix           *aPrefix,
+                                               const otExternalRouteConfig *aConfig)
+{
+    return AsCoreType(aInstance).Get<NetworkData::Publisher>().ReplacePublishedExternalRoute(
+        AsCoreType(aPrefix), AsCoreType(aConfig), NetworkData::Publisher::kFromUser);
+}
+
 bool otNetDataIsPrefixAdded(otInstance *aInstance, const otIp6Prefix *aPrefix)
 {
     return AsCoreType(aInstance).Get<NetworkData::Publisher>().IsPrefixAdded(AsCoreType(aPrefix));
diff --git a/src/core/api/netdiag_api.cpp b/src/core/api/netdiag_api.cpp
index b6f2d60..ffcc4c1 100644
--- a/src/core/api/netdiag_api.cpp
+++ b/src/core/api/netdiag_api.cpp
@@ -33,8 +33,6 @@
 
 #include "openthread-core-config.h"
 
-#if OPENTHREAD_FTD || OPENTHREAD_CONFIG_TMF_NETWORK_DIAG_MTD_ENABLE
-
 #include <openthread/netdiag.h>
 
 #include "common/as_core_type.hpp"
@@ -42,6 +40,8 @@
 
 using namespace ot;
 
+#if OPENTHREAD_CONFIG_TMF_NETDIAG_CLIENT_ENABLE
+
 otError otThreadGetNextDiagnosticTlv(const otMessage       *aMessage,
                                      otNetworkDiagIterator *aIterator,
                                      otNetworkDiagTlv      *aNetworkDiagTlv)
@@ -49,7 +49,7 @@
     AssertPointerIsNotNull(aIterator);
     AssertPointerIsNotNull(aNetworkDiagTlv);
 
-    return NetworkDiagnostic::NetworkDiagnostic::GetNextDiagTlv(AsCoapMessage(aMessage), *aIterator, *aNetworkDiagTlv);
+    return NetworkDiagnostic::Client::GetNextDiagTlv(AsCoapMessage(aMessage), *aIterator, *aNetworkDiagTlv);
 }
 
 otError otThreadSendDiagnosticGet(otInstance                    *aInstance,
@@ -59,7 +59,7 @@
                                   otReceiveDiagnosticGetCallback aCallback,
                                   void                          *aCallbackContext)
 {
-    return AsCoreType(aInstance).Get<NetworkDiagnostic::NetworkDiagnostic>().SendDiagnosticGet(
+    return AsCoreType(aInstance).Get<NetworkDiagnostic::Client>().SendDiagnosticGet(
         AsCoreType(aDestination), aTlvTypes, aCount, aCallback, aCallbackContext);
 }
 
@@ -68,8 +68,40 @@
                                     const uint8_t       aTlvTypes[],
                                     uint8_t             aCount)
 {
-    return AsCoreType(aInstance).Get<NetworkDiagnostic::NetworkDiagnostic>().SendDiagnosticReset(
-        AsCoreType(aDestination), aTlvTypes, aCount);
+    return AsCoreType(aInstance).Get<NetworkDiagnostic::Client>().SendDiagnosticReset(AsCoreType(aDestination),
+                                                                                      aTlvTypes, aCount);
 }
 
-#endif // OPENTHREAD_FTD || OPENTHREAD_CONFIG_TMF_NETWORK_DIAG_MTD_ENABLE
+#endif // OPENTHREAD_CONFIG_TMF_NETDIAG_CLIENT_ENABLE
+
+const char *otThreadGetVendorName(otInstance *aInstance)
+{
+    return AsCoreType(aInstance).Get<NetworkDiagnostic::Server>().GetVendorName();
+}
+
+const char *otThreadGetVendorModel(otInstance *aInstance)
+{
+    return AsCoreType(aInstance).Get<NetworkDiagnostic::Server>().GetVendorModel();
+}
+
+const char *otThreadGetVendorSwVersion(otInstance *aInstance)
+{
+    return AsCoreType(aInstance).Get<NetworkDiagnostic::Server>().GetVendorSwVersion();
+}
+
+#if OPENTHREAD_CONFIG_NET_DIAG_VENDOR_INFO_SET_API_ENABLE
+otError otThreadSetVendorName(otInstance *aInstance, const char *aVendorName)
+{
+    return AsCoreType(aInstance).Get<NetworkDiagnostic::Server>().SetVendorName(aVendorName);
+}
+
+otError otThreadSetVendorModel(otInstance *aInstance, const char *aVendorModel)
+{
+    return AsCoreType(aInstance).Get<NetworkDiagnostic::Server>().SetVendorModel(aVendorModel);
+}
+
+otError otThreadSetVendorSwVersion(otInstance *aInstance, const char *aVendorSwVersion)
+{
+    return AsCoreType(aInstance).Get<NetworkDiagnostic::Server>().SetVendorSwVersion(aVendorSwVersion);
+}
+#endif
diff --git a/src/core/api/thread_api.cpp b/src/core/api/thread_api.cpp
index 0d1a584..778a663 100644
--- a/src/core/api/thread_api.cpp
+++ b/src/core/api/thread_api.cpp
@@ -40,6 +40,7 @@
 #include "common/as_core_type.hpp"
 #include "common/debug.hpp"
 #include "common/locator_getters.hpp"
+#include "common/uptime.hpp"
 #include "thread/version.hpp"
 
 using namespace ot;
@@ -436,12 +437,14 @@
 
 void otThreadResetMleCounters(otInstance *aInstance) { AsCoreType(aInstance).Get<Mle::MleRouter>().ResetCounters(); }
 
+#if OPENTHREAD_CONFIG_MLE_PARENT_RESPONSE_CALLBACK_API_ENABLE
 void otThreadRegisterParentResponseCallback(otInstance                    *aInstance,
                                             otThreadParentResponseCallback aCallback,
                                             void                          *aContext)
 {
     AsCoreType(aInstance).Get<Mle::MleRouter>().RegisterParentResponseStatsCallback(aCallback, aContext);
 }
+#endif
 
 #if OPENTHREAD_CONFIG_TMF_ANYCAST_LOCATOR_ENABLE
 otError otThreadLocateAnycastDestination(otInstance                    *aInstance,
@@ -464,3 +467,12 @@
 }
 
 #endif // OPENTHREAD_FTD || OPENTHREAD_MTD
+
+#if OPENTHREAD_CONFIG_UPTIME_ENABLE
+void otConvertDurationInSecondsToString(uint32_t aDuration, char *aBuffer, uint16_t aSize)
+{
+    StringWriter writer(aBuffer, aSize);
+
+    Uptime::UptimeToString(Uptime::SecToMsec(aDuration), writer, /* aIncludeMsec */ false);
+}
+#endif
diff --git a/src/core/backbone_router/backbone_tmf.cpp b/src/core/backbone_router/backbone_tmf.cpp
index 1fe7ee5..d49e31c 100644
--- a/src/core/backbone_router/backbone_tmf.cpp
+++ b/src/core/backbone_router/backbone_tmf.cpp
@@ -55,6 +55,7 @@
     Error error = kErrorNone;
 
     SuccessOrExit(error = Coap::Start(kBackboneUdpPort, Ip6::kNetifBackbone));
+    LogInfo("Start listening on port %u", kBackboneUdpPort);
     SubscribeMulticast(Get<Local>().GetAllNetworkBackboneRoutersAddress());
 
 exit:
diff --git a/src/core/border_router/routing_manager.cpp b/src/core/border_router/routing_manager.cpp
index 8cae214..e713ec6 100644
--- a/src/core/border_router/routing_manager.cpp
+++ b/src/core/border_router/routing_manager.cpp
@@ -67,11 +67,12 @@
     , mIsRunning(false)
     , mIsEnabled(false)
     , mInfraIf(aInstance)
-    , mLocalOmrPrefix(aInstance)
+    , mOmrPrefixManager(aInstance)
     , mRioPreference(NetworkData::kRoutePreferenceLow)
     , mUserSetRioPreference(false)
     , mOnLinkPrefixManager(aInstance)
     , mDiscoveredPrefixTable(aInstance)
+    , mRoutePublisher(aInstance)
 #if OPENTHREAD_CONFIG_NAT64_BORDER_ROUTING_ENABLE
     , mNat64PrefixManager(aInstance)
 #endif
@@ -89,7 +90,7 @@
     SuccessOrExit(error = mInfraIf.Init(aInfraIfIndex));
 
     SuccessOrExit(error = LoadOrGenerateRandomBrUlaPrefix());
-    mLocalOmrPrefix.GenerateFrom(mBrUlaPrefix);
+    mOmrPrefixManager.Init(mBrUlaPrefix);
 #if OPENTHREAD_CONFIG_NAT64_BORDER_ROUTING_ENABLE
     mNat64PrefixManager.GenerateLocalPrefix(mBrUlaPrefix);
 #endif
@@ -179,7 +180,7 @@
     Error error = kErrorNone;
 
     VerifyOrExit(IsInitialized(), error = kErrorInvalidState);
-    aPrefix = mLocalOmrPrefix.GetPrefix();
+    aPrefix = mOmrPrefixManager.GetLocalPrefix().GetPrefix();
 
 exit:
     return error;
@@ -190,8 +191,8 @@
     Error error = kErrorNone;
 
     VerifyOrExit(IsRunning(), error = kErrorInvalidState);
-    aPrefix     = mFavoredOmrPrefix.GetPrefix();
-    aPreference = mFavoredOmrPrefix.GetPreference();
+    aPrefix     = mOmrPrefixManager.GetFavoredPrefix().GetPrefix();
+    aPreference = mOmrPrefixManager.GetFavoredPrefix().GetPreference();
 
 exit:
     return error;
@@ -308,6 +309,8 @@
         mIsRunning = true;
         UpdateDiscoveredPrefixTableOnNetDataChange();
         mOnLinkPrefixManager.Start();
+        mOmrPrefixManager.Start();
+        mRoutePublisher.Start();
         mRsSender.Start();
 #if OPENTHREAD_CONFIG_NAT64_BORDER_ROUTING_ENABLE
         mNat64PrefixManager.Start();
@@ -319,9 +322,7 @@
 {
     VerifyOrExit(mIsRunning);
 
-    mLocalOmrPrefix.RemoveFromNetData();
-    mFavoredOmrPrefix.Clear();
-
+    mOmrPrefixManager.Stop();
     mOnLinkPrefixManager.Stop();
 
 #if OPENTHREAD_CONFIG_NAT64_BORDER_ROUTING_ENABLE
@@ -341,6 +342,8 @@
 
     mRoutingPolicyTimer.Stop();
 
+    mRoutePublisher.Stop();
+
     LogInfo("Border Routing manager stopped");
 
     mIsRunning = false;
@@ -407,6 +410,7 @@
     if (aEvents.Contains(kEventThreadRoleChanged) && !mUserSetRioPreference)
     {
         SetRioPreferenceBasedOnRole();
+        mRoutePublisher.HandleRoleChanged();
     }
 
     VerifyOrExit(IsInitialized() && IsEnabled());
@@ -436,7 +440,6 @@
 {
     NetworkData::Iterator           iterator = NetworkData::kIteratorInit;
     NetworkData::OnMeshPrefixConfig prefixConfig;
-    bool                            foundDefRouteOmrPrefix = false;
 
     // Remove all OMR prefixes in Network Data from the
     // discovered prefix table. Also check if we have
@@ -450,137 +453,7 @@
         }
 
         mDiscoveredPrefixTable.RemoveRoutePrefix(prefixConfig.GetPrefix());
-
-        if (prefixConfig.mDefaultRoute)
-        {
-            foundDefRouteOmrPrefix = true;
-        }
     }
-
-    // If we find an OMR prefix with default route flag, it indicates
-    // that this prefix can be used with default route (routable beyond
-    // infra link).
-    //
-    // `DiscoveredPrefixTable` will always track which routers provide
-    // default route when processing received RA messages, but only
-    // if we see an OMR prefix with default route flag, we allow it
-    // to publish the discovered default route (as ::/0 external
-    // route) in Network Data.
-
-    mDiscoveredPrefixTable.SetAllowDefaultRouteInNetData(foundDefRouteOmrPrefix);
-}
-
-void RoutingManager::EvaluateOmrPrefix(void)
-{
-    NetworkData::Iterator           iterator = NetworkData::kIteratorInit;
-    NetworkData::OnMeshPrefixConfig onMeshPrefixConfig;
-
-    OT_ASSERT(mIsRunning);
-
-    mFavoredOmrPrefix.Clear();
-
-    while (Get<NetworkData::Leader>().GetNextOnMeshPrefix(iterator, onMeshPrefixConfig) == kErrorNone)
-    {
-        if (!IsValidOmrPrefix(onMeshPrefixConfig) || !onMeshPrefixConfig.mPreferred)
-        {
-            continue;
-        }
-
-        if (mFavoredOmrPrefix.IsEmpty() || !mFavoredOmrPrefix.IsFavoredOver(onMeshPrefixConfig))
-        {
-            mFavoredOmrPrefix.SetFrom(onMeshPrefixConfig);
-        }
-    }
-
-    // Decide if we need to add or remove our local OMR prefix.
-
-    if (mFavoredOmrPrefix.IsEmpty())
-    {
-        LogInfo("EvaluateOmrPrefix: No preferred OMR prefix found in Thread network");
-
-        // The `mFavoredOmrPrefix` remains empty if we fail to publish
-        // the local OMR prefix.
-        SuccessOrExit(mLocalOmrPrefix.AddToNetData());
-
-        mFavoredOmrPrefix.SetFrom(mLocalOmrPrefix);
-    }
-    else if (mFavoredOmrPrefix.GetPrefix() == mLocalOmrPrefix.GetPrefix())
-    {
-        IgnoreError(mLocalOmrPrefix.AddToNetData());
-    }
-    else if (mLocalOmrPrefix.IsAddedInNetData())
-    {
-        LogInfo("EvaluateOmrPrefix: There is already a preferred OMR prefix %s in the Thread network",
-                mFavoredOmrPrefix.GetPrefix().ToString().AsCString());
-
-        mLocalOmrPrefix.RemoveFromNetData();
-    }
-
-exit:
-    return;
-}
-
-void RoutingManager::EvaluatePublishingPrefix(const Ip6::Prefix &aPrefix)
-{
-    // This method evaluates whether to publish or unpublish a given
-    // `aPrefix` as an external route in the Network Data. It makes a
-    // collective decision by checking with different sub-components to
-    // see whether or not each wants this prefix published and if so
-    // at what preference level and flags.
-    //
-    // Before calling this method, the sub-components need to make sure
-    // to update their internal state such that their `ShouldPublish()`
-    // provides the correct info.
-
-    bool                             shouldPublish = false;
-    NetworkData::ExternalRouteConfig routeConfig;
-
-    routeConfig.Clear();
-    routeConfig.SetPrefix(aPrefix);
-    routeConfig.mPreference = NetworkData::kRoutePreferenceLow;
-    routeConfig.mStable     = true;
-
-    VerifyOrExit(mIsRunning);
-
-    // The order of checks is important. The Discovered Prefix Table is
-    // first followed by Local On Link Prefix manager and finally NAT64
-    // prefix manager.
-
-    shouldPublish = mDiscoveredPrefixTable.ShouldPublish(routeConfig);
-
-    if (mOnLinkPrefixManager.ShouldPublish(routeConfig))
-    {
-        shouldPublish = true;
-    }
-
-#if OPENTHREAD_CONFIG_NAT64_BORDER_ROUTING_ENABLE
-    if (mNat64PrefixManager.ShouldPublish(routeConfig))
-    {
-        shouldPublish = true;
-    }
-#endif
-
-    if (shouldPublish)
-    {
-        SuccessOrAssert(Get<NetworkData::Publisher>().PublishExternalRoute(
-            routeConfig, NetworkData::Publisher::kFromRoutingManager));
-    }
-    else
-    {
-        UnpublishExternalRoute(aPrefix);
-    }
-
-exit:
-    return;
-}
-
-void RoutingManager::UnpublishExternalRoute(const Ip6::Prefix &aPrefix)
-{
-    VerifyOrExit(mIsRunning);
-    IgnoreError(Get<NetworkData::Publisher>().UnpublishPrefix(aPrefix));
-
-exit:
-    return;
 }
 
 // This method evaluate the routing policy depends on prefix and route
@@ -594,7 +467,8 @@
     LogInfo("Evaluating routing policy");
 
     mOnLinkPrefixManager.Evaluate();
-    EvaluateOmrPrefix();
+    mOmrPrefixManager.Evaluate();
+    mRoutePublisher.Evaluate();
 #if OPENTHREAD_CONFIG_NAT64_BORDER_ROUTING_ENABLE
     mNat64PrefixManager.Evaluate();
 #endif
@@ -624,7 +498,8 @@
     // the emitted Router Advert message on infrastructure side
     // and published in the Thread Network Data.
 
-    return mIsRunning && !mFavoredOmrPrefix.IsEmpty() && mOnLinkPrefixManager.IsInitalEvaluationDone();
+    return mIsRunning && !mOmrPrefixManager.GetFavoredPrefix().IsEmpty() &&
+           mOnLinkPrefixManager.IsInitalEvaluationDone();
 }
 
 void RoutingManager::ScheduleRoutingPolicyEvaluation(ScheduleMode aMode)
@@ -708,7 +583,8 @@
 
         while (Get<NetworkData::Leader>().GetNextOnMeshPrefix(iterator, prefixConfig) == kErrorNone)
         {
-            if (!prefixConfig.mOnMesh || prefixConfig.mDp || (prefixConfig.GetPrefix() == mLocalOmrPrefix.GetPrefix()))
+            if (!prefixConfig.mOnMesh || prefixConfig.mDp ||
+                (prefixConfig.GetPrefix() == mOmrPrefixManager.GetLocalPrefix().GetPrefix()))
             {
                 continue;
             }
@@ -716,9 +592,9 @@
             mAdvertisedPrefixes.MarkAsDeleted(prefixConfig.GetPrefix());
         }
 
-        if (mLocalOmrPrefix.IsAddedInNetData())
+        if (mOmrPrefixManager.IsLocalAddedInNetData())
         {
-            mAdvertisedPrefixes.MarkAsDeleted(mLocalOmrPrefix.GetPrefix());
+            mAdvertisedPrefixes.MarkAsDeleted(mOmrPrefixManager.GetLocalPrefix().GetPrefix());
         }
     }
 
@@ -746,16 +622,16 @@
 
         // (1) Local OMR prefix.
 
-        if (mLocalOmrPrefix.IsAddedInNetData())
+        if (mOmrPrefixManager.IsLocalAddedInNetData())
         {
-            mAdvertisedPrefixes.Add(mLocalOmrPrefix.GetPrefix());
+            mAdvertisedPrefixes.Add(mOmrPrefixManager.GetLocalPrefix().GetPrefix());
         }
 
         // (2) Favored OMR prefix.
 
-        if (!mFavoredOmrPrefix.IsEmpty() && !mFavoredOmrPrefix.IsDomainPrefix())
+        if (!mOmrPrefixManager.GetFavoredPrefix().IsEmpty() && !mOmrPrefixManager.GetFavoredPrefix().IsDomainPrefix())
         {
-            mAdvertisedPrefixes.Add(mFavoredOmrPrefix.GetPrefix());
+            mAdvertisedPrefixes.Add(mOmrPrefixManager.GetFavoredPrefix().GetPrefix());
         }
 
         // (3) All other OMR prefixes.
@@ -765,8 +641,8 @@
         while (Get<NetworkData::Leader>().GetNextOnMeshPrefix(iterator, prefixConfig) == kErrorNone)
         {
             // Local OMR prefix is added to the array depending on
-            // `mLocalOmrPrefix.IsAddedInNetData()` at step (1). As
-            // we iterate through the Network Data prefixes, we skip
+            // `mOmrPrefixManager.IsLocalAddedInNetData()` at step (1).
+            // As we iterate through the Network Data prefixes, we skip
             // over entries matching the local OMR prefix. This
             // ensures that we stop including it in emitted RA
             // message as soon as we decide to remove it from Network
@@ -780,7 +656,8 @@
                 continue;
             }
 
-            if (IsValidOmrPrefix(prefixConfig) && (prefixConfig.GetPrefix() != mLocalOmrPrefix.GetPrefix()))
+            if (IsValidOmrPrefix(prefixConfig) &&
+                (prefixConfig.GetPrefix() != mOmrPrefixManager.GetLocalPrefix().GetPrefix()))
             {
                 mAdvertisedPrefixes.Add(prefixConfig.GetPrefix());
             }
@@ -1060,7 +937,7 @@
         ExitNow();
     }
 
-    VerifyOrExit(mLocalOmrPrefix.GetPrefix() != aPrefix);
+    VerifyOrExit(mOmrPrefixManager.GetLocalPrefix().GetPrefix() != aPrefix);
 
     // Ignore OMR prefixes advertised by ourselves or in current Thread Network Data.
     // The `mAdvertisedPrefixes` and the OMR prefix set in Network Data should eventually
@@ -1090,6 +967,7 @@
 
     ResetDiscoveredPrefixStaleTimer();
     mOnLinkPrefixManager.HandleDiscoveredPrefixTableChanged();
+    mRoutePublisher.Evaluate();
 
 exit:
     return;
@@ -1113,15 +991,19 @@
     return contains;
 }
 
-bool RoutingManager::NetworkDataContainsExternalRoute(const Ip6::Prefix &aPrefix) const
+bool RoutingManager::NetworkDataContainsUlaRoute(void) const
 {
+    // Determine whether leader Network Data contains a route
+    // prefix which is either the ULA prefix `fc00::/7` or
+    // a sub-prefix of it (e.g., default route).
+
     NetworkData::Iterator            iterator = NetworkData::kIteratorInit;
     NetworkData::ExternalRouteConfig routeConfig;
     bool                             contains = false;
 
     while (Get<NetworkData::Leader>().GetNextExternalRoute(iterator, routeConfig) == kErrorNone)
     {
-        if (routeConfig.mStable && routeConfig.GetPrefix() == aPrefix)
+        if (routeConfig.mStable && RoutePublisher::GetUlaPrefix().ContainsPrefix(routeConfig.GetPrefix()))
         {
             contains = true;
             break;
@@ -1223,7 +1105,6 @@
     , mEntryTimer(aInstance)
     , mRouterTimer(aInstance)
     , mSignalTask(aInstance)
-    , mAllowDefaultRouteInNetData(false)
 {
 }
 
@@ -1311,7 +1192,6 @@
     }
 
     mEntryTimer.FireAtIfEarlier(entry->GetExpireTime());
-    Get<RoutingManager>().EvaluatePublishingPrefix(entry->GetPrefix());
 
     SignalTableChanged();
 
@@ -1354,11 +1234,10 @@
         Entry newEntry;
 
         newEntry.SetFrom(aPio);
-        entry->AdoptValidAndPreferredLiftimesFrom(newEntry);
+        entry->AdoptValidAndPreferredLifetimesFrom(newEntry);
     }
 
     mEntryTimer.FireAtIfEarlier(entry->GetExpireTime());
-    Get<RoutingManager>().EvaluatePublishingPrefix(entry->GetPrefix());
 
     SignalTableChanged();
 
@@ -1402,7 +1281,6 @@
     }
 
     mEntryTimer.FireAtIfEarlier(entry->GetExpireTime());
-    Get<RoutingManager>().EvaluatePublishingPrefix(entry->GetPrefix());
 
     SignalTableChanged();
 
@@ -1410,21 +1288,35 @@
     return;
 }
 
-void RoutingManager::DiscoveredPrefixTable::SetAllowDefaultRouteInNetData(bool aAllow)
+bool RoutingManager::DiscoveredPrefixTable::Contains(const Entry::Checker &aChecker) const
 {
-    Ip6::Prefix prefix;
+    bool contains = false;
 
-    VerifyOrExit(aAllow != mAllowDefaultRouteInNetData);
+    for (const Router &router : mRouters)
+    {
+        if (router.mEntries.ContainsMatching(aChecker))
+        {
+            contains = true;
+            break;
+        }
+    }
 
-    LogInfo("Allow default route in netdata: %s -> %s", ToYesNo(mAllowDefaultRouteInNetData), ToYesNo(aAllow));
+    return contains;
+}
 
-    mAllowDefaultRouteInNetData = aAllow;
+bool RoutingManager::DiscoveredPrefixTable::ContainsDefaultOrNonUlaRoutePrefix(void) const
+{
+    return Contains(Entry::Checker(Entry::Checker::kIsNotUla, Entry::kTypeRoute));
+}
 
-    prefix.Clear();
-    Get<RoutingManager>().EvaluatePublishingPrefix(prefix);
+bool RoutingManager::DiscoveredPrefixTable::ContainsNonUlaOnLinkPrefix(void) const
+{
+    return Contains(Entry::Checker(Entry::Checker::kIsNotUla, Entry::kTypeOnLink));
+}
 
-exit:
-    return;
+bool RoutingManager::DiscoveredPrefixTable::ContainsUlaOnLinkPrefix(void) const
+{
+    return Contains(Entry::Checker(Entry::Checker::kIsUla, Entry::kTypeOnLink));
 }
 
 void RoutingManager::DiscoveredPrefixTable::FindFavoredOnLinkPrefix(Ip6::Prefix &aPrefix) const
@@ -1452,32 +1344,6 @@
     }
 }
 
-bool RoutingManager::DiscoveredPrefixTable::ContainsOnLinkPrefix(const Ip6::Prefix &aPrefix) const
-{
-    return ContainsPrefix(Entry::Matcher(aPrefix, Entry::kTypeOnLink));
-}
-
-bool RoutingManager::DiscoveredPrefixTable::ContainsRoutePrefix(const Ip6::Prefix &aPrefix) const
-{
-    return ContainsPrefix(Entry::Matcher(aPrefix, Entry::kTypeRoute));
-}
-
-bool RoutingManager::DiscoveredPrefixTable::ContainsPrefix(const Entry::Matcher &aMatcher) const
-{
-    bool contains = false;
-
-    for (const Router &router : mRouters)
-    {
-        if (router.mEntries.ContainsMatching(aMatcher))
-        {
-            contains = true;
-            break;
-        }
-    }
-
-    return contains;
-}
-
 void RoutingManager::DiscoveredPrefixTable::RemoveOnLinkPrefix(const Ip6::Prefix &aPrefix)
 {
     RemovePrefix(Entry::Matcher(aPrefix, Entry::kTypeOnLink));
@@ -1504,8 +1370,6 @@
     FreeEntries(removedEntries);
     RemoveRoutersWithNoEntries();
 
-    Get<RoutingManager>().EvaluatePublishingPrefix(aMatcher.mPrefix);
-
     SignalTableChanged();
 
 exit:
@@ -1523,7 +1387,6 @@
 
         while ((entry = router.mEntries.Pop()) != nullptr)
         {
-            Get<RoutingManager>().UnpublishExternalRoute(entry->GetPrefix());
             FreeEntry(*entry);
             SignalTableChanged();
         }
@@ -1672,29 +1535,6 @@
     return favoredEntry;
 }
 
-bool RoutingManager::DiscoveredPrefixTable::ShouldPublish(NetworkData::ExternalRouteConfig &aRouteConfig) const
-{
-    bool         shouldPublish = false;
-    const Entry *favoredEntry;
-
-    if (aRouteConfig.GetPrefix().GetLength() == 0)
-    {
-        // If the change is to default route ::/0 prefix, make sure we
-        // are allowed to publish default route in Network Data.
-
-        VerifyOrExit(mAllowDefaultRouteInNetData);
-    }
-
-    favoredEntry = FindFavoredEntryToPublish(aRouteConfig.GetPrefix());
-    VerifyOrExit(favoredEntry != nullptr);
-
-    shouldPublish            = true;
-    aRouteConfig.mPreference = favoredEntry->GetPreference();
-
-exit:
-    return shouldPublish;
-}
-
 void RoutingManager::DiscoveredPrefixTable::HandleEntryTimer(void) { RemoveExpiredEntries(); }
 
 void RoutingManager::DiscoveredPrefixTable::RemoveExpiredEntries(void)
@@ -1710,14 +1550,6 @@
 
     RemoveRoutersWithNoEntries();
 
-    // Determine if we need to publish/unpublish any prefixes in
-    // the Network Data.
-
-    for (const Entry &expiredEntry : expiredEntries)
-    {
-        Get<RoutingManager>().EvaluatePublishingPrefix(expiredEntry.GetPrefix());
-    }
-
     if (!expiredEntries.IsEmpty())
     {
         SignalTableChanged();
@@ -1764,7 +1596,7 @@
 void RoutingManager::DiscoveredPrefixTable::UpdateRouterOnRx(Router &aRouter)
 {
     aRouter.mNsProbeCount = 0;
-    aRouter.mTimeout      = TimerMilli::GetNow() + Random::NonCrypto::AddJitter(Router::kActiveTimout, Router::kJitter);
+    aRouter.mTimeout = TimerMilli::GetNow() + Random::NonCrypto::AddJitter(Router::kActiveTimeout, Router::kJitter);
 
     mRouterTimer.FireAtIfEarlier(aRouter.mTimeout);
 }
@@ -1805,7 +1637,7 @@
             }
 
             router.mTimeout = now + ((router.mNsProbeCount < Router::kMaxNsProbes) ? Router::kNsProbeRetryInterval
-                                                                                   : Router::kNsProbeTimout);
+                                                                                   : Router::kNsProbeTimeout);
 
             SendNeighborSolicitToRouter(router);
         }
@@ -1927,9 +1759,14 @@
     return (mType == aMatcher.mType) && (mPrefix == aMatcher.mPrefix);
 }
 
-bool RoutingManager::DiscoveredPrefixTable::Entry::Matches(const ExpirationChecker &aCheker) const
+bool RoutingManager::DiscoveredPrefixTable::Entry::Matches(const Checker &aChecker) const
 {
-    return GetExpireTime() <= aCheker.mNow;
+    return (mType == aChecker.mType) && (mPrefix.IsUniqueLocal() == (aChecker.mMode == Checker::kIsUla));
+}
+
+bool RoutingManager::DiscoveredPrefixTable::Entry::Matches(const ExpirationChecker &aChecker) const
+{
+    return GetExpireTime() <= aChecker.mNow;
 }
 
 TimeMilli RoutingManager::DiscoveredPrefixTable::Entry::GetExpireTime(void) const
@@ -1959,7 +1796,7 @@
     return IsOnLinkPrefix() ? NetworkData::kRoutePreferenceMedium : GetRoutePreference();
 }
 
-void RoutingManager::DiscoveredPrefixTable::Entry::AdoptValidAndPreferredLiftimesFrom(const Entry &aEntry)
+void RoutingManager::DiscoveredPrefixTable::Entry::AdoptValidAndPreferredLifetimesFrom(const Entry &aEntry)
 {
     constexpr uint32_t kTwoHoursInSeconds = 2 * 3600;
 
@@ -2004,23 +1841,32 @@
 }
 
 //---------------------------------------------------------------------------------------------------------------------
-// OmrPrefix
+// FavoredOmrPrefix
 
-void RoutingManager::OmrPrefix::SetFrom(const NetworkData::OnMeshPrefixConfig &aOnMeshPrefixConfig)
+bool RoutingManager::FavoredOmrPrefix::IsInfrastructureDerived(void) const
+{
+    // Indicate whether the OMR prefix is infrastructure-derived which
+    // can be identified as a valid OMR prefix with preference of
+    // medium or higher.
+
+    return !IsEmpty() && (mPreference >= NetworkData::kRoutePreferenceMedium);
+}
+
+void RoutingManager::FavoredOmrPrefix::SetFrom(const NetworkData::OnMeshPrefixConfig &aOnMeshPrefixConfig)
 {
     mPrefix         = aOnMeshPrefixConfig.GetPrefix();
     mPreference     = aOnMeshPrefixConfig.GetPreference();
     mIsDomainPrefix = aOnMeshPrefixConfig.mDp;
 }
 
-void RoutingManager::OmrPrefix::SetFrom(const LocalOmrPrefix &aLocalOmrPrefix)
+void RoutingManager::FavoredOmrPrefix::SetFrom(const OmrPrefix &aOmrPrefix)
 {
-    mPrefix         = aLocalOmrPrefix.GetPrefix();
-    mPreference     = aLocalOmrPrefix.GetPreference();
-    mIsDomainPrefix = false;
+    mPrefix         = aOmrPrefix.GetPrefix();
+    mPreference     = aOmrPrefix.GetPreference();
+    mIsDomainPrefix = aOmrPrefix.IsDomainPrefix();
 }
 
-bool RoutingManager::OmrPrefix::IsFavoredOver(const NetworkData::OnMeshPrefixConfig &aOmrPrefixConfig) const
+bool RoutingManager::FavoredOmrPrefix::IsFavoredOver(const NetworkData::OnMeshPrefixConfig &aOmrPrefixConfig) const
 {
     // This method determines whether this OMR prefix is favored
     // over another prefix. A prefix with higher preference is
@@ -2040,79 +1886,181 @@
 }
 
 //---------------------------------------------------------------------------------------------------------------------
-// LocalOmrPrefix
+// OmrPrefixManager
 
-RoutingManager::LocalOmrPrefix::LocalOmrPrefix(Instance &aInstance)
+RoutingManager::OmrPrefixManager::OmrPrefixManager(Instance &aInstance)
     : InstanceLocator(aInstance)
-    , mIsAddedInNetData(false)
+    , mIsLocalAddedInNetData(false)
+    , mDefaultRoute(false)
 {
 }
 
-void RoutingManager::LocalOmrPrefix::GenerateFrom(const Ip6::Prefix &aBrUlaPrefix)
+void RoutingManager::OmrPrefixManager::Init(const Ip6::Prefix &aBrUlaPrefix)
 {
-    mPrefix = aBrUlaPrefix;
-    mPrefix.SetSubnetId(kOmrPrefixSubnetId);
-    mPrefix.SetLength(kOmrPrefixLength);
+    mLocalPrefix.mPrefix = aBrUlaPrefix;
+    mLocalPrefix.mPrefix.SetSubnetId(kOmrPrefixSubnetId);
+    mLocalPrefix.mPrefix.SetLength(kOmrPrefixLength);
+    mLocalPrefix.mPreference     = NetworkData::kRoutePreferenceLow;
+    mLocalPrefix.mIsDomainPrefix = false;
 
-    LogInfo("Generated OMR prefix: %s", mPrefix.ToString().AsCString());
+    LogInfo("Generated local OMR prefix: %s", mLocalPrefix.mPrefix.ToString().AsCString());
 }
 
-Error RoutingManager::LocalOmrPrefix::AddToNetData(void)
+void RoutingManager::OmrPrefixManager::Start(void) { DetermineFavoredPrefix(); }
+
+void RoutingManager::OmrPrefixManager::Stop(void)
 {
-    Error                           error = kErrorNone;
-    NetworkData::OnMeshPrefixConfig config;
+    RemoveLocalFromNetData();
+    mFavoredPrefix.Clear();
+}
 
-    VerifyOrExit(!mIsAddedInNetData);
+void RoutingManager::OmrPrefixManager::DetermineFavoredPrefix(void)
+{
+    // Determine the favored OMR prefix present in Network Data.
 
-    config.Clear();
-    config.mPrefix       = mPrefix;
-    config.mStable       = true;
-    config.mSlaac        = true;
-    config.mPreferred    = true;
-    config.mOnMesh       = true;
-    config.mDefaultRoute = false;
-    config.mPreference   = GetPreference();
+    NetworkData::Iterator           iterator = NetworkData::kIteratorInit;
+    NetworkData::OnMeshPrefixConfig prefixConfig;
 
-    error = Get<NetworkData::Local>().AddOnMeshPrefix(config);
+    mFavoredPrefix.Clear();
 
-    if (error != kErrorNone)
+    while (Get<NetworkData::Leader>().GetNextOnMeshPrefix(iterator, prefixConfig) == kErrorNone)
     {
-        LogWarn("Failed to add local OMR prefix %s in Thread Network Data: %s", mPrefix.ToString().AsCString(),
-                ErrorToString(error));
-        ExitNow();
+        if (!IsValidOmrPrefix(prefixConfig) || !prefixConfig.mPreferred)
+        {
+            continue;
+        }
+
+        if (mFavoredPrefix.IsEmpty() || !mFavoredPrefix.IsFavoredOver(prefixConfig))
+        {
+            mFavoredPrefix.SetFrom(prefixConfig);
+        }
+    }
+}
+
+void RoutingManager::OmrPrefixManager::Evaluate(void)
+{
+    OT_ASSERT(Get<RoutingManager>().IsRunning());
+
+    DetermineFavoredPrefix();
+
+    // Decide if we need to add or remove our local OMR prefix.
+
+    if (mFavoredPrefix.IsEmpty())
+    {
+        LogInfo("No favored OMR prefix found in Thread network");
+
+        // The `mFavoredPrefix` remains empty if we fail to publish
+        // the local OMR prefix.
+        SuccessOrExit(AddLocalToNetData());
+
+        mFavoredPrefix.SetFrom(mLocalPrefix);
+    }
+    else if (mFavoredPrefix.GetPrefix() == mLocalPrefix.GetPrefix())
+    {
+        IgnoreError(AddLocalToNetData());
+    }
+    else if (mIsLocalAddedInNetData)
+    {
+        LogInfo("There is already a favored OMR prefix %s in the Thread network",
+                mFavoredPrefix.GetPrefix().ToString().AsCString());
+
+        RemoveLocalFromNetData();
     }
 
-    mIsAddedInNetData = true;
-    Get<NetworkData::Notifier>().HandleServerDataUpdated();
-    LogInfo("Added local OMR prefix %s in Thread Network Data", mPrefix.ToString().AsCString());
+exit:
+    return;
+}
+
+Error RoutingManager::OmrPrefixManager::AddLocalToNetData(void)
+{
+    Error error = kErrorNone;
+
+    VerifyOrExit(!mIsLocalAddedInNetData);
+    SuccessOrExit(error = AddOrUpdateLocalInNetData());
+    mIsLocalAddedInNetData = true;
 
 exit:
     return error;
 }
 
-void RoutingManager::LocalOmrPrefix::RemoveFromNetData(void)
+Error RoutingManager::OmrPrefixManager::AddOrUpdateLocalInNetData(void)
 {
-    Error error = kErrorNone;
+    // Add the local OMR prefix in Thread Network Data or update it
+    // (e.g., change default route flag) if it is already added.
 
-    VerifyOrExit(mIsAddedInNetData);
+    Error                           error;
+    NetworkData::OnMeshPrefixConfig config;
 
-    error = Get<NetworkData::Local>().RemoveOnMeshPrefix(mPrefix);
+    config.Clear();
+    config.mPrefix       = mLocalPrefix.GetPrefix();
+    config.mStable       = true;
+    config.mSlaac        = true;
+    config.mPreferred    = true;
+    config.mOnMesh       = true;
+    config.mDefaultRoute = mDefaultRoute;
+    config.mPreference   = mLocalPrefix.GetPreference();
+
+    error = Get<NetworkData::Local>().AddOnMeshPrefix(config);
 
     if (error != kErrorNone)
     {
-        LogWarn("Failed to remove local OMR prefix %s from Thread Network Data: %s", mPrefix.ToString().AsCString(),
-                ErrorToString(error));
+        LogWarn("Failed to %s %s in Thread Network Data: %s", !mIsLocalAddedInNetData ? "add" : "update",
+                LocalToString().AsCString(), ErrorToString(error));
         ExitNow();
     }
 
-    mIsAddedInNetData = false;
     Get<NetworkData::Notifier>().HandleServerDataUpdated();
-    LogInfo("Removed local OMR prefix %s from Thread Network Data", mPrefix.ToString().AsCString());
+
+    LogInfo("%s %s in Thread Network Data", !mIsLocalAddedInNetData ? "Added" : "Updated", LocalToString().AsCString());
+
+exit:
+    return error;
+}
+
+void RoutingManager::OmrPrefixManager::RemoveLocalFromNetData(void)
+{
+    Error error = kErrorNone;
+
+    VerifyOrExit(mIsLocalAddedInNetData);
+
+    error = Get<NetworkData::Local>().RemoveOnMeshPrefix(mLocalPrefix.GetPrefix());
+
+    if (error != kErrorNone)
+    {
+        LogWarn("Failed to remove %s from Thread Network Data: %s", LocalToString().AsCString(), ErrorToString(error));
+        ExitNow();
+    }
+
+    mIsLocalAddedInNetData = false;
+    Get<NetworkData::Notifier>().HandleServerDataUpdated();
+    LogInfo("Removed %s from Thread Network Data", LocalToString().AsCString());
 
 exit:
     return;
 }
 
+void RoutingManager::OmrPrefixManager::UpdateDefaultRouteFlag(bool aDefaultRoute)
+{
+    VerifyOrExit(aDefaultRoute != mDefaultRoute);
+
+    mDefaultRoute = aDefaultRoute;
+
+    VerifyOrExit(mIsLocalAddedInNetData);
+    IgnoreError(AddOrUpdateLocalInNetData());
+
+exit:
+    return;
+}
+
+RoutingManager::OmrPrefixManager::InfoString RoutingManager::OmrPrefixManager::LocalToString(void) const
+{
+    InfoString string;
+
+    string.Append("local OMR prefix %s (def-route:%s)", mLocalPrefix.GetPrefix().ToString().AsCString(),
+                  ToYesNo(mDefaultRoute));
+    return string;
+}
+
 //---------------------------------------------------------------------------------------------------------------------
 // OnLinkPrefixManager
 
@@ -2228,25 +2176,12 @@
     return;
 }
 
-void RoutingManager::OnLinkPrefixManager::Start(void)
-{
-    Get<RoutingManager>().EvaluatePublishingPrefix(mLocalPrefix);
-
-    for (const OldPrefix &oldPrefix : mOldLocalPrefixes)
-    {
-        Get<RoutingManager>().EvaluatePublishingPrefix(oldPrefix.mPrefix);
-    }
-}
+void RoutingManager::OnLinkPrefixManager::Start(void) {}
 
 void RoutingManager::OnLinkPrefixManager::Stop(void)
 {
     mFavoredDiscoveredPrefix.Clear();
 
-    for (const OldPrefix &oldPrefix : mOldLocalPrefixes)
-    {
-        Get<RoutingManager>().UnpublishExternalRoute(oldPrefix.mPrefix);
-    }
-
     switch (mState)
     {
     case kIdle:
@@ -2255,7 +2190,6 @@
     case kPublishing:
     case kAdvertising:
     case kDeprecating:
-        Get<RoutingManager>().UnpublishExternalRoute(mLocalPrefix);
         mState = kDeprecating;
         break;
     }
@@ -2360,16 +2294,15 @@
 
     mState = kPublishing;
     ResetExpireTime(TimerMilli::GetNow());
-    LogInfo("Publishing local on-link prefix %s in netdata", mLocalPrefix.ToString().AsCString());
 
-    Get<RoutingManager>().EvaluatePublishingPrefix(mLocalPrefix);
+    // We wait for the ULA `fc00::/7` route or a sub-prefix of it (e.g.,
+    // default route) to be added in Network Data before
+    // starting to advertise the local on-link prefix in RAs.
+    // However, if it is already present in Network Data (e.g.,
+    // added by another BR on the same Thread mesh), we can
+    // immediately start advertising it.
 
-    // We wait for the prefix to be added in Network Data before
-    // starting to advertise it in RAs. However, if it is already
-    // present in Network Data (e.g., added by another BR on the same
-    // Thread mesh), we can immediately start advertising it.
-
-    if (Get<RoutingManager>().NetworkDataContainsExternalRoute(mLocalPrefix))
+    if (Get<RoutingManager>().NetworkDataContainsUlaRoute())
     {
         EnterAdvertisingState();
     }
@@ -2401,31 +2334,14 @@
     }
 }
 
-bool RoutingManager::OnLinkPrefixManager::ShouldPublish(NetworkData::ExternalRouteConfig &aRouteConfig) const
+bool RoutingManager::OnLinkPrefixManager::ShouldPublishUlaRoute(void) const
 {
-    bool shouldPublish = false;
+    // Determine whether or not we should publish ULA prefix. We need
+    // to publish if we are in any of `kPublishing`, `kAdvertising`,
+    // or `kDeprecating` states, or if there is at least one old local
+    // prefix being deprecated.
 
-    if (aRouteConfig.GetPrefix() == mLocalPrefix)
-    {
-        switch (mState)
-        {
-        case kIdle:
-            break;
-        case kPublishing:
-        case kAdvertising:
-        case kDeprecating:
-            shouldPublish            = true;
-            aRouteConfig.mPreference = NetworkData::kRoutePreferenceMedium;
-            break;
-        }
-    }
-    else if (mOldLocalPrefixes.ContainsMatching(aRouteConfig.GetPrefix()))
-    {
-        shouldPublish            = true;
-        aRouteConfig.mPreference = NetworkData::kRoutePreferenceMedium;
-    }
-
-    return shouldPublish;
+    return (mState != kIdle) || !mOldLocalPrefixes.IsEmpty();
 }
 
 void RoutingManager::OnLinkPrefixManager::ResetExpireTime(TimeMilli aNow)
@@ -2515,7 +2431,7 @@
 {
     VerifyOrExit(mState == kPublishing);
 
-    if (Get<RoutingManager>().NetworkDataContainsExternalRoute(mLocalPrefix))
+    if (Get<RoutingManager>().NetworkDataContainsUlaRoute())
     {
         EnterAdvertisingState();
         Get<RoutingManager>().ScheduleRoutingPolicyEvaluation(kAfterRandomDelay);
@@ -2528,7 +2444,7 @@
 void RoutingManager::OnLinkPrefixManager::HandleExtPanIdChange(void)
 {
     // If the current local prefix is being advertised or deprecated,
-    // we save it in `mOldLocalPrefixes` and keep deprecating it . It will
+    // we save it in `mOldLocalPrefixes` and keep deprecating it. It will
     // be included in emitted RAs as PIO with zero preferred lifetime.
     // It will still be present in Network Data until its expire time
     // so to allow Thread nodes to continue to communicate with `InfraIf`
@@ -2544,10 +2460,7 @@
     switch (oldState)
     {
     case kIdle:
-        break;
-
     case kPublishing:
-        Get<RoutingManager>().EvaluatePublishingPrefix(oldPrefix);
         break;
 
     case kAdvertising:
@@ -2558,6 +2471,7 @@
 
     if (Get<RoutingManager>().mIsRunning)
     {
+        Get<RoutingManager>().mRoutePublisher.Evaluate();
         Get<RoutingManager>().ScheduleRoutingPolicyEvaluation(kAfterRandomDelay);
     }
 
@@ -2606,13 +2520,6 @@
 
     SavePrefix(aPrefix, aExpireTime);
 
-    Get<RoutingManager>().EvaluatePublishingPrefix(aPrefix);
-
-    if (removedPrefix.GetLength() != 0)
-    {
-        Get<RoutingManager>().EvaluatePublishingPrefix(removedPrefix);
-    }
-
 exit:
     return;
 }
@@ -2644,7 +2551,6 @@
             LogInfo("Local on-link prefix %s expired", mLocalPrefix.ToString().AsCString());
             IgnoreError(Get<Settings>().RemoveBrOnLinkPrefix(mLocalPrefix));
             mState = kIdle;
-            Get<RoutingManager>().EvaluatePublishingPrefix(mLocalPrefix);
         }
         else
         {
@@ -2670,14 +2576,14 @@
         LogInfo("Old local on-link prefix %s expired", prefix.ToString().AsCString());
         IgnoreError(Get<Settings>().RemoveBrOnLinkPrefix(prefix));
         mOldLocalPrefixes.RemoveMatching(prefix);
-
-        Get<RoutingManager>().EvaluatePublishingPrefix(prefix);
     }
 
     if (nextExpireTime != now.GetDistantFuture())
     {
         mTimer.FireAtIfEarlier(nextExpireTime);
     }
+
+    Get<RoutingManager>().mRoutePublisher.Evaluate();
 }
 
 //---------------------------------------------------------------------------------------------------------------------
@@ -2716,6 +2622,189 @@
     }
 }
 
+//---------------------------------------------------------------------------------------------------------------------
+// RoutePublisher
+
+const otIp6Prefix RoutingManager::RoutePublisher::kUlaPrefix = {
+    {{{0xfc, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}}},
+    7,
+};
+
+RoutingManager::RoutePublisher::RoutePublisher(Instance &aInstance)
+    : InstanceLocator(aInstance)
+    , mState(kDoNotPublish)
+    , mPreference(NetworkData::kRoutePreferenceMedium)
+    , mUserSetPreference(false)
+{
+}
+
+void RoutingManager::RoutePublisher::Evaluate(void)
+{
+    State newState = kDoNotPublish;
+
+    VerifyOrExit(Get<RoutingManager>().IsRunning());
+
+    if (Get<RoutingManager>().mOmrPrefixManager.GetFavoredPrefix().IsInfrastructureDerived() &&
+        Get<RoutingManager>().mDiscoveredPrefixTable.ContainsDefaultOrNonUlaRoutePrefix())
+    {
+        newState = kPublishDefault;
+    }
+    else if (Get<RoutingManager>().mDiscoveredPrefixTable.ContainsNonUlaOnLinkPrefix())
+    {
+        newState = kPublishDefault;
+    }
+    else if (Get<RoutingManager>().mDiscoveredPrefixTable.ContainsUlaOnLinkPrefix() ||
+             Get<RoutingManager>().mOnLinkPrefixManager.ShouldPublishUlaRoute())
+    {
+        newState = kPublishUla;
+    }
+
+exit:
+    if (newState != mState)
+    {
+        LogInfo("RoutePublisher state: %s -> %s", StateToString(mState), StateToString(newState));
+        UpdatePublishedRoute(newState);
+        Get<RoutingManager>().mOmrPrefixManager.UpdateDefaultRouteFlag(newState == kPublishDefault);
+    }
+}
+
+void RoutingManager::RoutePublisher::DeterminePrefixFor(State aState, Ip6::Prefix &aPrefix) const
+{
+    aPrefix.Clear();
+
+    switch (aState)
+    {
+    case kDoNotPublish:
+    case kPublishDefault:
+        // `Clear()` will set the prefix `::/0`.
+        break;
+    case kPublishUla:
+        aPrefix = GetUlaPrefix();
+        break;
+    }
+}
+
+void RoutingManager::RoutePublisher::UpdatePublishedRoute(State aNewState)
+{
+    // Updates the published route entry in Network Data, transitioning
+    // from current `mState` to new `aNewState`. This method can be used
+    // when there is no change to `mState` but a change to `mPreference`.
+
+    Ip6::Prefix                      oldPrefix;
+    NetworkData::ExternalRouteConfig routeConfig;
+
+    DeterminePrefixFor(mState, oldPrefix);
+
+    if (aNewState == kDoNotPublish)
+    {
+        VerifyOrExit(mState != kDoNotPublish);
+        IgnoreError(Get<NetworkData::Publisher>().UnpublishPrefix(oldPrefix));
+        ExitNow();
+    }
+
+    routeConfig.Clear();
+    routeConfig.mPreference = mPreference;
+    routeConfig.mStable     = true;
+    DeterminePrefixFor(aNewState, routeConfig.GetPrefix());
+
+    // If we were not publishing a route prefix before, publish the new
+    // `routeConfig`. Otherwise, use `ReplacePublishedExternalRoute()` to
+    // replace the previously published prefix entry. This ensures that we do
+    // not have a situation where the previous route is removed while the new
+    // one is not yet added in the Network Data.
+
+    if (mState == kDoNotPublish)
+    {
+        SuccessOrAssert(Get<NetworkData::Publisher>().PublishExternalRoute(
+            routeConfig, NetworkData::Publisher::kFromRoutingManager));
+    }
+    else
+    {
+        SuccessOrAssert(Get<NetworkData::Publisher>().ReplacePublishedExternalRoute(
+            oldPrefix, routeConfig, NetworkData::Publisher::kFromRoutingManager));
+    }
+
+exit:
+    mState = aNewState;
+}
+
+void RoutingManager::RoutePublisher::Unpublish(void)
+{
+    // Unpublish the previously published route based on `mState`
+    // and update `mState`.
+
+    Ip6::Prefix prefix;
+
+    VerifyOrExit(mState != kDoNotPublish);
+    DeterminePrefixFor(mState, prefix);
+    IgnoreError(Get<NetworkData::Publisher>().UnpublishPrefix(prefix));
+    mState = kDoNotPublish;
+
+exit:
+    return;
+}
+
+void RoutingManager::RoutePublisher::SetPreference(RoutePreference aPreference)
+{
+    LogInfo("User explicitly set published route preference to %s", RoutePreferenceToString(aPreference));
+    mUserSetPreference = true;
+    UpdatePreference(aPreference);
+}
+
+void RoutingManager::RoutePublisher::ClearPreference(void)
+{
+    VerifyOrExit(mUserSetPreference);
+
+    LogInfo("User cleared explicitly set published route preference - set based on role");
+    mUserSetPreference = false;
+    SetPreferenceBasedOnRole();
+
+exit:
+    return;
+}
+
+void RoutingManager::RoutePublisher::SetPreferenceBasedOnRole(void)
+{
+    UpdatePreference(Get<Mle::Mle>().IsRouterOrLeader() ? NetworkData::kRoutePreferenceMedium
+                                                        : NetworkData::kRoutePreferenceLow);
+}
+
+void RoutingManager::RoutePublisher::HandleRoleChanged(void)
+{
+    if (!mUserSetPreference)
+    {
+        SetPreferenceBasedOnRole();
+    }
+}
+
+void RoutingManager::RoutePublisher::UpdatePreference(RoutePreference aPreference)
+{
+    VerifyOrExit(mPreference != aPreference);
+
+    LogInfo("Published route preference changed: %s -> %s", RoutePreferenceToString(mPreference),
+            RoutePreferenceToString(aPreference));
+    mPreference = aPreference;
+    UpdatePublishedRoute(mState);
+
+exit:
+    return;
+}
+
+const char *RoutingManager::RoutePublisher::StateToString(State aState)
+{
+    static const char *const kStateStrings[] = {
+        "none",      // (0) kDoNotPublish
+        "def-route", // (1) kPublishDefault
+        "ula",       // (2) kPublishUla
+    };
+
+    static_assert(0 == kDoNotPublish, "kDoNotPublish value is incorrect");
+    static_assert(1 == kPublishDefault, "kPublishDefault value is incorrect");
+    static_assert(2 == kPublishUla, "kPublishUla value is incorrect");
+
+    return kStateStrings[aState];
+}
+
 #if OPENTHREAD_CONFIG_NAT64_BORDER_ROUTING_ENABLE
 
 //---------------------------------------------------------------------------------------------------------------------
@@ -2768,7 +2857,7 @@
 
     if (mPublishedPrefix.IsValidNat64())
     {
-        Get<RoutingManager>().UnpublishExternalRoute(mPublishedPrefix);
+        IgnoreError(Get<NetworkData::Publisher>().UnpublishPrefix(mPublishedPrefix));
     }
 
     mPublishedPrefix.Clear();
@@ -2792,16 +2881,15 @@
 
 const Ip6::Prefix &RoutingManager::Nat64PrefixManager::GetFavoredPrefix(RoutePreference &aPreference) const
 {
-    const Ip6::Prefix *favoredPrefix = &mInfraIfPrefix;
+    const Ip6::Prefix *favoredPrefix = &mLocalPrefix;
 
-    if (mInfraIfPrefix.IsValidNat64())
+    aPreference = NetworkData::kRoutePreferenceLow;
+
+    if (mInfraIfPrefix.IsValidNat64() &&
+        Get<RoutingManager>().mOmrPrefixManager.GetFavoredPrefix().IsInfrastructureDerived())
     {
-        aPreference = NetworkData::kRoutePreferenceMedium;
-    }
-    else
-    {
-        favoredPrefix = &mLocalPrefix;
-        aPreference   = NetworkData::kRoutePreferenceLow;
+        favoredPrefix = &mInfraIfPrefix;
+        aPreference   = NetworkData::kRoutePreferenceMedium;
     }
 
     return *favoredPrefix;
@@ -2843,18 +2931,15 @@
 
     if (mPublishedPrefix.IsValidNat64() && (!shouldPublish || (prefix != mPublishedPrefix)))
     {
-        Ip6::Prefix prevPrefix;
-
-        prevPrefix = mPublishedPrefix;
+        IgnoreError(Get<NetworkData::Publisher>().UnpublishPrefix(mPublishedPrefix));
         mPublishedPrefix.Clear();
-        Get<RoutingManager>().EvaluatePublishingPrefix(prevPrefix);
     }
 
     if (shouldPublish && ((prefix != mPublishedPrefix) || (preference != mPublishedPreference)))
     {
         mPublishedPrefix     = prefix;
         mPublishedPreference = preference;
-        Get<RoutingManager>().EvaluatePublishingPrefix(mPublishedPrefix);
+        Publish();
     }
 
 #if OPENTHREAD_CONFIG_NAT64_TRANSLATOR_ENABLE
@@ -2874,19 +2959,18 @@
     return;
 }
 
-bool RoutingManager::Nat64PrefixManager::ShouldPublish(NetworkData::ExternalRouteConfig &aRouteConfig) const
+void RoutingManager::Nat64PrefixManager::Publish(void)
 {
-    bool shouldPublish = false;
+    NetworkData::ExternalRouteConfig routeConfig;
 
-    VerifyOrExit(mPublishedPrefix.IsValidNat64());
-    VerifyOrExit(mPublishedPrefix == aRouteConfig.GetPrefix());
+    routeConfig.Clear();
+    routeConfig.SetPrefix(mPublishedPrefix);
+    routeConfig.mPreference = mPublishedPreference;
+    routeConfig.mStable     = true;
+    routeConfig.mNat64      = true;
 
-    shouldPublish            = true;
-    aRouteConfig.mPreference = mPublishedPreference;
-    aRouteConfig.mNat64      = true;
-
-exit:
-    return shouldPublish;
+    SuccessOrAssert(
+        Get<NetworkData::Publisher>().PublishExternalRoute(routeConfig, NetworkData::Publisher::kFromRoutingManager));
 }
 
 void RoutingManager::Nat64PrefixManager::HandleTimer(void)
diff --git a/src/core/border_router/routing_manager.hpp b/src/core/border_router/routing_manager.hpp
index 5ed94e2..213721c 100644
--- a/src/core/border_router/routing_manager.hpp
+++ b/src/core/border_router/routing_manager.hpp
@@ -92,14 +92,12 @@
      * This is used by `NetworkData::Publisher` to reserve entries for use by `RoutingManager`.
      *
      * The number of published entries accounts for:
-     * - Max number of discovered prefix entries,
-     * - One entry for local on-link prefixes,
-     * - Max number of old (deprecating) local on-link prefixes,
+     * - Route prefix `fc00::/7` or `::/0`
      * - One entry for NAT64 published prefix.
+     * - One extra entry for transitions.
      *
      */
-    static constexpr uint16_t kMaxPublishedPrefixes = OPENTHREAD_CONFIG_BORDER_ROUTING_MAX_DISCOVERED_PREFIXES + 1 +
-                                                      OPENTHREAD_CONFIG_BORDER_ROUTING_MAX_OLD_ON_LINK_PREFIXES + 1;
+    static constexpr uint16_t kMaxPublishedPrefixes = 3;
 
     /**
      * This enumeration represents the states of `RoutingManager`.
@@ -292,7 +290,7 @@
     Nat64::State GetNat64PrefixManagerState(void) const { return mNat64PrefixManager.GetState(); }
 
     /**
-     * Enable or disable NAT64 orefix publishing.
+     * Enable or disable NAT64 prefix publishing.
      *
      * @param[in]  aEnabled   A boolean to enable/disable NAT64 prefix publishing.
      *
@@ -358,7 +356,7 @@
      * @param[in] aOnMeshPrefixConfig  The on-mesh prefix configuration to check.
      *
      * @retval   TRUE    The prefix is a valid OMR prefix.
-     * @retval   FALE    The prefix is not a valid OMR prefix.
+     * @retval   FALSE   The prefix is not a valid OMR prefix.
      *
      */
     static bool IsValidOmrPrefix(const NetworkData::OnMeshPrefixConfig &aOnMeshPrefixConfig);
@@ -369,7 +367,7 @@
      * @param[in]  aPrefix  The prefix to check.
      *
      * @retval   TRUE    The prefix is a valid OMR prefix.
-     * @retval   FALE    The prefix is not a valid OMR prefix.
+     * @retval   FALSE   The prefix is not a valid OMR prefix.
      *
      */
     static bool IsValidOmrPrefix(const Ip6::Prefix &aPrefix);
@@ -501,17 +499,15 @@
                                         const Ip6::Address                 &aSrcAddress);
         void ProcessNeighborAdvertMessage(const Ip6::Nd::NeighborAdvertMessage &aNaMessage);
 
-        void SetAllowDefaultRouteInNetData(bool aAllow);
+        bool ContainsDefaultOrNonUlaRoutePrefix(void) const;
+        bool ContainsNonUlaOnLinkPrefix(void) const;
+        bool ContainsUlaOnLinkPrefix(void) const;
 
         void FindFavoredOnLinkPrefix(Ip6::Prefix &aPrefix) const;
-        bool ContainsOnLinkPrefix(const Ip6::Prefix &aPrefix) const;
+
         void RemoveOnLinkPrefix(const Ip6::Prefix &aPrefix);
-
-        bool ContainsRoutePrefix(const Ip6::Prefix &aPrefix) const;
         void RemoveRoutePrefix(const Ip6::Prefix &aPrefix);
 
-        bool ShouldPublish(NetworkData::ExternalRouteConfig &aRouteConfig) const;
-
         void RemoveAllEntries(void);
         void RemoveOrDeprecateOldEntries(TimeMilli aTimeThreshold);
 
@@ -548,7 +544,26 @@
                 }
 
                 const Ip6::Prefix &mPrefix;
-                bool               mType;
+                Type               mType;
+            };
+
+            struct Checker
+            {
+                enum Mode : uint8_t
+                {
+                    kIsUla,
+                    kIsNotUla,
+                };
+
+                Checker(Mode aMode, Type aType)
+                    : mMode(aMode)
+                    , mType(aType)
+
+                {
+                }
+
+                Mode mMode;
+                Type mType;
             };
 
             struct ExpirationChecker
@@ -566,6 +581,7 @@
             void               SetFrom(const Ip6::Nd::RouteInfoOption &aRio);
             Type               GetType(void) const { return mType; }
             bool               IsOnLinkPrefix(void) const { return (mType == kTypeOnLink); }
+            bool               IsRoutePrefix(void) const { return (mType == kTypeRoute); }
             const Ip6::Prefix &GetPrefix(void) const { return mPrefix; }
             const TimeMilli   &GetLastUpdateTime(void) const { return mLastUpdateTime; }
             uint32_t           GetValidLifetime(void) const { return mValidLifetime; }
@@ -575,13 +591,14 @@
             RoutePreference    GetPreference(void) const;
             bool               operator==(const Entry &aOther) const;
             bool               Matches(const Matcher &aMatcher) const;
-            bool               Matches(const ExpirationChecker &aCheker) const;
+            bool               Matches(const Checker &aChecker) const;
+            bool               Matches(const ExpirationChecker &aChecker) const;
 
             // Methods to use when `IsOnLinkPrefix()`
             uint32_t GetPreferredLifetime(void) const { return mShared.mPreferredLifetime; }
             void     ClearPreferredLifetime(void) { mShared.mPreferredLifetime = 0; }
             bool     IsDeprecated(void) const;
-            void     AdoptValidAndPreferredLiftimesFrom(const Entry &aEntry);
+            void     AdoptValidAndPreferredLifetimesFrom(const Entry &aEntry);
 
             // Method to use when `!IsOnlinkPrefix()`
             RoutePreference GetRoutePreference(void) const { return mShared.mRoutePreference; }
@@ -605,11 +622,11 @@
         {
             // The timeout (in msec) for router staying in active state
             // before starting the Neighbor Solicitation (NS) probes.
-            static constexpr uint32_t kActiveTimout = OPENTHREAD_CONFIG_BORDER_ROUTING_ROUTER_ACTIVE_CHECK_TIMEOUT;
+            static constexpr uint32_t kActiveTimeout = OPENTHREAD_CONFIG_BORDER_ROUTING_ROUTER_ACTIVE_CHECK_TIMEOUT;
 
             static constexpr uint8_t  kMaxNsProbes          = 5;    // Max number of NS probe attempts.
             static constexpr uint32_t kNsProbeRetryInterval = 1000; // In msec. Time between NS probe attempts.
-            static constexpr uint32_t kNsProbeTimout        = 2000; // In msec. Max Wait time after last NS probe.
+            static constexpr uint32_t kNsProbeTimeout       = 2000; // In msec. Max Wait time after last NS probe.
             static constexpr uint32_t kJitter               = 2000; // In msec. Jitter to randomize probe starts.
 
             static_assert(kMaxNsProbes < 255, "kMaxNsProbes MUST not be 255");
@@ -642,7 +659,7 @@
         void         ProcessDefaultRoute(const Ip6::Nd::RouterAdvertMessage::Header &aRaHeader, Router &aRouter);
         void         ProcessPrefixInfoOption(const Ip6::Nd::PrefixInfoOption &aPio, Router &aRouter);
         void         ProcessRouteInfoOption(const Ip6::Nd::RouteInfoOption &aRio, Router &aRouter);
-        bool         ContainsPrefix(const Entry::Matcher &aMatcher) const;
+        bool         Contains(const Entry::Checker &aChecker) const;
         void         RemovePrefix(const Entry::Matcher &aMatcher);
         void         RemoveOrDeprecateEntriesFromInactiveRouters(void);
         void         RemoveRoutersWithNoEntries(void);
@@ -665,44 +682,70 @@
         EntryTimer                 mEntryTimer;
         RouterTimer                mRouterTimer;
         SignalTask                 mSignalTask;
-        bool                       mAllowDefaultRouteInNetData;
     };
 
-    class LocalOmrPrefix;
+    class OmrPrefixManager;
 
     class OmrPrefix : public Clearable<OmrPrefix>
     {
+        friend class OmrPrefixManager;
+
     public:
         OmrPrefix(void) { Clear(); }
 
         bool               IsEmpty(void) const { return (mPrefix.GetLength() == 0); }
-        void               SetFrom(const NetworkData::OnMeshPrefixConfig &aOnMeshPrefixConfig);
-        void               SetFrom(const LocalOmrPrefix &aLocalOmrPrefix);
         const Ip6::Prefix &GetPrefix(void) const { return mPrefix; }
         RoutePreference    GetPreference(void) const { return mPreference; }
-        bool               IsFavoredOver(const NetworkData::OnMeshPrefixConfig &aOmrPrefixConfig) const;
         bool               IsDomainPrefix(void) const { return mIsDomainPrefix; }
 
-    private:
+    protected:
         Ip6::Prefix     mPrefix;
         RoutePreference mPreference;
         bool            mIsDomainPrefix;
     };
 
-    class LocalOmrPrefix : public InstanceLocator
+    class FavoredOmrPrefix : public OmrPrefix
     {
+        friend class OmrPrefixManager;
+
     public:
-        explicit LocalOmrPrefix(Instance &aInstance);
-        void               GenerateFrom(const Ip6::Prefix &aBrUlaPrefix);
-        const Ip6::Prefix &GetPrefix(void) const { return mPrefix; }
-        RoutePreference    GetPreference(void) const { return NetworkData::kRoutePreferenceLow; }
-        Error              AddToNetData(void);
-        void               RemoveFromNetData(void);
-        bool               IsAddedInNetData(void) const { return mIsAddedInNetData; }
+        bool IsInfrastructureDerived(void) const;
 
     private:
-        Ip6::Prefix mPrefix;
-        bool        mIsAddedInNetData;
+        void SetFrom(const NetworkData::OnMeshPrefixConfig &aOnMeshPrefixConfig);
+        void SetFrom(const OmrPrefix &aOmrPrefix);
+        bool IsFavoredOver(const NetworkData::OnMeshPrefixConfig &aOmrPrefixConfig) const;
+    };
+
+    class OmrPrefixManager : public InstanceLocator
+    {
+    public:
+        explicit OmrPrefixManager(Instance &aInstance);
+
+        void                    Init(const Ip6::Prefix &aBrUlaPrefix);
+        void                    Start(void);
+        void                    Stop(void);
+        void                    Evaluate(void);
+        void                    UpdateDefaultRouteFlag(bool aDefaultRoute);
+        bool                    IsLocalAddedInNetData(void) const { return mIsLocalAddedInNetData; }
+        const OmrPrefix        &GetLocalPrefix(void) const { return mLocalPrefix; }
+        const FavoredOmrPrefix &GetFavoredPrefix(void) const { return mFavoredPrefix; }
+
+    private:
+        static constexpr uint16_t kInfoStringSize = 85;
+
+        typedef String<kInfoStringSize> InfoString;
+
+        void       DetermineFavoredPrefix(void);
+        Error      AddLocalToNetData(void);
+        Error      AddOrUpdateLocalInNetData(void);
+        void       RemoveLocalFromNetData(void);
+        InfoString LocalToString(void) const;
+
+        OmrPrefix        mLocalPrefix;
+        FavoredOmrPrefix mFavoredPrefix;
+        bool             mIsLocalAddedInNetData;
+        bool             mDefaultRoute;
     };
 
     void HandleOnLinkPrefixManagerTimer(void) { mOnLinkPrefixManager.HandleTimer(); }
@@ -723,7 +766,7 @@
         const Ip6::Prefix &GetFavoredDiscoveredPrefix(void) const { return mFavoredDiscoveredPrefix; }
         bool               IsInitalEvaluationDone(void) const;
         void               HandleDiscoveredPrefixTableChanged(void);
-        bool               ShouldPublish(NetworkData::ExternalRouteConfig &aRouteConfig) const;
+        bool               ShouldPublishUlaRoute(void) const;
         void               AppendAsPiosTo(Ip6::Nd::RouterAdvertMessage &aRaMessage);
         bool               IsPublishingOrAdvertising(void) const;
         void               HandleNetDataChange(void);
@@ -788,7 +831,7 @@
         // lifetime, and selection of the NAT64 prefix to publish in
         // Network Data.
         //
-        // Calling methods except GeneratrLocalPrefix and SetEnabled
+        // Calling methods except GenerateLocalPrefix and SetEnabled
         // when disabled becomes no-op.
 
         explicit Nat64PrefixManager(Instance &aInstance);
@@ -803,12 +846,12 @@
         const Ip6::Prefix &GetLocalPrefix(void) const { return mLocalPrefix; }
         const Ip6::Prefix &GetFavoredPrefix(RoutePreference &aPreference) const;
         void               Evaluate(void);
-        bool               ShouldPublish(NetworkData::ExternalRouteConfig &aRouteConfig) const;
         void               HandleDiscoverDone(const Ip6::Prefix &aPrefix);
         void               HandleTimer(void);
 
     private:
         void Discover(void);
+        void Publish(void);
 
         using Nat64Timer = TimerMilliIn<RoutingManager, &RoutingManager::HandleNat64PrefixManagerTimer>;
 
@@ -822,6 +865,46 @@
     };
 #endif // OPENTHREAD_CONFIG_NAT64_BORDER_ROUTING_ENABLE
 
+    class RoutePublisher : public InstanceLocator // Manages the routes that are published in net data
+    {
+    public:
+        explicit RoutePublisher(Instance &aInstance);
+
+        void Start(void) { Evaluate(); }
+        void Stop(void) { Unpublish(); }
+        void Evaluate(void);
+
+        RoutePreference GetPreference(void) const { return mPreference; }
+        void            SetPreference(RoutePreference aPreference);
+        void            ClearPreference(void);
+
+        void HandleRoleChanged(void);
+
+        static const Ip6::Prefix &GetUlaPrefix(void) { return AsCoreType(&kUlaPrefix); }
+
+    private:
+        static const otIp6Prefix kUlaPrefix;
+
+        enum State : uint8_t
+        {
+            kDoNotPublish,   // Do not publish any routes in network data.
+            kPublishDefault, // Publish "::/0" route in network data.
+            kPublishUla,     // Publish "fc00::/7" route in network data.
+        };
+
+        void DeterminePrefixFor(State aState, Ip6::Prefix &aPrefix) const;
+        void UpdatePublishedRoute(State aNewState);
+        void Unpublish(void);
+        void SetPreferenceBasedOnRole(void);
+        void UpdatePreference(RoutePreference aPreference);
+
+        static const char *StateToString(State aState);
+
+        State           mState;
+        RoutePreference mPreference;
+        bool            mUserSetPreference;
+    };
+
     struct RaInfo
     {
         // Tracks info about emitted RA messages: Number of RAs sent,
@@ -895,9 +978,6 @@
     void EvaluateRoutingPolicy(void);
     bool IsInitalPolicyEvaluationDone(void) const;
     void ScheduleRoutingPolicyEvaluation(ScheduleMode aMode);
-    void EvaluateOmrPrefix(void);
-    void EvaluatePublishingPrefix(const Ip6::Prefix &aPrefix);
-    void UnpublishExternalRoute(const Ip6::Prefix &aPrefix);
     void HandleRsSenderFinished(TimeMilli aStartTime);
     void SendRouterAdvertisement(RouterAdvTxMode aRaTxMode);
 
@@ -910,7 +990,7 @@
     bool ShouldProcessRouteInfoOption(const Ip6::Nd::RouteInfoOption &aRio, const Ip6::Prefix &aPrefix);
     void UpdateDiscoveredPrefixTableOnNetDataChange(void);
     bool NetworkDataContainsOmrPrefix(const Ip6::Prefix &aPrefix) const;
-    bool NetworkDataContainsExternalRoute(const Ip6::Prefix &aPrefix) const;
+    bool NetworkDataContainsUlaRoute(void) const;
     void UpdateRouterAdvertHeader(const Ip6::Nd::RouterAdvertMessage *aRouterAdvertMessage);
     bool IsReceivedRouterAdvertFromManager(const Ip6::Nd::RouterAdvertMessage &aRaMessage) const;
     void ResetDiscoveredPrefixStaleTimer(void);
@@ -935,8 +1015,7 @@
     // randomly generated if none is found in persistent storage.
     Ip6::Prefix mBrUlaPrefix;
 
-    LocalOmrPrefix mLocalOmrPrefix;
-    OmrPrefix      mFavoredOmrPrefix;
+    OmrPrefixManager mOmrPrefixManager;
 
     // List of on-mesh prefixes (discovered from Network Data) which
     // were advertised as RIO in the last sent RA message.
@@ -949,6 +1028,8 @@
 
     DiscoveredPrefixTable mDiscoveredPrefixTable;
 
+    RoutePublisher mRoutePublisher;
+
 #if OPENTHREAD_CONFIG_NAT64_BORDER_ROUTING_ENABLE
     Nat64PrefixManager mNat64PrefixManager;
 #endif
diff --git a/src/core/coap/coap.cpp b/src/core/coap/coap.cpp
index b76fb37..38e5618 100644
--- a/src/core/coap/coap.cpp
+++ b/src/core/coap/coap.cpp
@@ -115,6 +115,13 @@
     return message;
 }
 
+Message *CoapBase::NewMessage(void) { return NewMessage(Message::Settings::GetDefault()); }
+
+Message *CoapBase::NewPriorityMessage(void)
+{
+    return NewMessage(Message::Settings(Message::kWithLinkSecurity, Message::kPriorityNet));
+}
+
 Message *CoapBase::NewPriorityConfirmablePostMessage(Uri aUri)
 {
     return InitMessage(NewPriorityMessage(), kTypeConfirmable, aUri);
@@ -351,6 +358,11 @@
     return error;
 }
 
+Error CoapBase::SendMessage(Message &aMessage, const Ip6::MessageInfo &aMessageInfo, const TxParameters &aTxParameters)
+{
+    return SendMessage(aMessage, aMessageInfo, aTxParameters, nullptr, nullptr);
+}
+
 Error CoapBase::SendMessage(Message                &aMessage,
                             const Ip6::MessageInfo &aMessageInfo,
                             ResponseHandler         aHandler,
@@ -363,6 +375,11 @@
 #endif
 }
 
+Error CoapBase::SendMessage(Message &aMessage, const Ip6::MessageInfo &aMessageInfo)
+{
+    return SendMessage(aMessage, aMessageInfo, nullptr, nullptr);
+}
+
 Error CoapBase::SendReset(Message &aRequest, const Ip6::MessageInfo &aMessageInfo)
 {
     return SendEmptyMessage(kTypeReset, aRequest, aMessageInfo);
@@ -378,6 +395,11 @@
     return (aRequest.IsConfirmable() ? SendHeaderResponse(aCode, aRequest, aMessageInfo) : kErrorInvalidArgs);
 }
 
+Error CoapBase::SendEmptyAck(const Message &aRequest, const Ip6::MessageInfo &aMessageInfo)
+{
+    return SendEmptyAck(aRequest, aMessageInfo, kCodeChanged);
+}
+
 Error CoapBase::SendNotFound(const Message &aRequest, const Ip6::MessageInfo &aMessageInfo)
 {
     return SendHeaderResponse(kCodeNotFound, aRequest, aMessageInfo);
@@ -1059,8 +1081,8 @@
     bool responseObserve = false;
 #endif
 #if OPENTHREAD_CONFIG_COAP_BLOCKWISE_TRANSFER_ENABLE
-    uint8_t  blockOptionType    = 0;
-    uint32_t totalTransfereSize = 0;
+    uint8_t  blockOptionType   = 0;
+    uint32_t totalTransferSize = 0;
 #endif
 
     request = FindRelatedRequest(aMessage, aMessageInfo, metadata);
@@ -1159,7 +1181,7 @@
 
                         case kOptionSize2:
                             // ToDo: wait for method to read uint option values
-                            totalTransfereSize = 0;
+                            totalTransferSize = 0;
                             break;
 
                         default:
@@ -1190,8 +1212,8 @@
                 case 2: // Block2 option
                     if (aMessage.GetCode() < kCodeBadRequest && metadata.mBlockwiseReceiveHook != nullptr)
                     {
-                        error = SendNextBlock2Request(*request, aMessage, aMessageInfo, metadata, totalTransfereSize,
-                                                      false);
+                        error =
+                            SendNextBlock2Request(*request, aMessage, aMessageInfo, metadata, totalTransferSize, false);
                     }
 
                     if (aMessage.GetCode() >= kCodeBadRequest || metadata.mBlockwiseReceiveHook == nullptr ||
@@ -1204,7 +1226,7 @@
                     if (aMessage.GetCode() < kCodeBadRequest && metadata.mBlockwiseReceiveHook != nullptr)
                     {
                         error =
-                            SendNextBlock2Request(*request, aMessage, aMessageInfo, metadata, totalTransfereSize, true);
+                            SendNextBlock2Request(*request, aMessage, aMessageInfo, metadata, totalTransferSize, true);
                     }
 
                     FinalizeCoapTransaction(*request, metadata, &aMessage, &aMessageInfo, error);
@@ -1273,9 +1295,9 @@
     Error    error          = kErrorNone;
 #if OPENTHREAD_CONFIG_COAP_BLOCKWISE_TRANSFER_ENABLE
     Option::Iterator iterator;
-    char            *curUriPath         = uriPath;
-    uint8_t          blockOptionType    = 0;
-    uint32_t         totalTransfereSize = 0;
+    char            *curUriPath        = uriPath;
+    uint8_t          blockOptionType   = 0;
+    uint32_t         totalTransferSize = 0;
 #endif
 
     if (mInterceptor.IsSet())
@@ -1328,7 +1350,7 @@
 
         case kOptionSize1:
             // ToDo: wait for method to read uint option values
-            totalTransfereSize = 0;
+            totalTransferSize = 0;
             break;
 
         default:
@@ -1354,7 +1376,7 @@
             case 1:
                 if (resource.mReceiveHook != nullptr)
                 {
-                    switch (ProcessBlock1Request(aMessage, aMessageInfo, resource, totalTransfereSize))
+                    switch (ProcessBlock1Request(aMessage, aMessageInfo, resource, totalTransferSize))
                     {
                     case kErrorNone:
                         resource.HandleRequest(aMessage, aMessageInfo);
@@ -1621,7 +1643,7 @@
     if ((mAckRandomFactorDenominator > 0) && (mAckRandomFactorNumerator >= mAckRandomFactorDenominator) &&
         (mAckTimeout >= OT_COAP_MIN_ACK_TIMEOUT) && (mMaxRetransmit <= OT_COAP_MAX_RETRANSMIT))
     {
-        // Calulate exchange lifetime step by step and verify no overflow.
+        // Calculate exchange lifetime step by step and verify no overflow.
         uint32_t tmp = Multiply(mAckTimeout, (1U << (mMaxRetransmit + 1)) - 1);
 
         tmp = Multiply(tmp, mAckRandomFactorNumerator);
diff --git a/src/core/coap/coap.hpp b/src/core/coap/coap.hpp
index 5a35770..b5d05a6 100644
--- a/src/core/coap/coap.hpp
+++ b/src/core/coap/coap.hpp
@@ -449,7 +449,15 @@
      * @returns A pointer to the message or `nullptr` if failed to allocate message.
      *
      */
-    Message *NewMessage(const Message::Settings &aSettings = Message::Settings::GetDefault());
+    Message *NewMessage(const Message::Settings &aSettings);
+
+    /**
+     * This method allocates a new message with a CoAP header with default settings.
+     *
+     * @returns A pointer to the message or `nullptr` if failed to allocate message.
+     *
+     */
+    Message *NewMessage(void);
 
     /**
      * This method allocates a new message with a CoAP header that has Network Control priority level.
@@ -457,10 +465,7 @@
      * @returns A pointer to the message or `nullptr` if failed to allocate message.
      *
      */
-    Message *NewPriorityMessage(void)
-    {
-        return NewMessage(Message::Settings(Message::kWithLinkSecurity, Message::kPriorityNet));
-    }
+    Message *NewPriorityMessage(void);
 
     /**
      * This method allocates and initializes a new CoAP Confirmable Post message with Network Control priority level.
@@ -557,7 +562,7 @@
      *
      * If a response for a request is expected, respective function and context information should be provided.
      * If no response is expected, these arguments should be NULL pointers.
-     * If Message Id was not set in the header (equal to 0), this function will assign unique Message Id to the message.
+     * If Message ID was not set in the header (equal to 0), this method will assign unique Message ID to the message.
      *
      * @param[in]  aMessage      A reference to the message to send.
      * @param[in]  aMessageInfo  A reference to the message info associated with @p aMessage.
@@ -585,7 +590,7 @@
      *
      * If a response for a request is expected, respective function and context information should be provided.
      * If no response is expected, these arguments should be `nullptr` pointers.
-     * If Message Id was not set in the header (equal to 0), this function will assign unique Message Id to the message.
+     * If Message ID was not set in the header (equal to 0), this method will assign unique Message ID to the message.
      *
      * @param[in]  aMessage      A reference to the message to send.
      * @param[in]  aMessageInfo  A reference to the message info associated with @p aMessage.
@@ -600,16 +605,28 @@
     Error SendMessage(Message                &aMessage,
                       const Ip6::MessageInfo &aMessageInfo,
                       const TxParameters     &aTxParameters,
-                      ResponseHandler         aHandler = nullptr,
-                      void                   *aContext = nullptr);
+                      ResponseHandler         aHandler,
+                      void                   *aContext);
 #endif // OPENTHREAD_CONFIG_COAP_BLOCKWISE_TRANSFER_ENABLE
 
     /**
+     * This method sends a CoAP message with custom transmission parameters.
+     *
+     * If Message ID was not set in the header (equal to 0), this method will assign unique Message ID to the message.
+     *
+     * @param[in]  aMessage      A reference to the message to send.
+     * @param[in]  aMessageInfo  A reference to the message info associated with @p aMessage.
+     * @param[in]  aTxParameters A reference to transmission parameters for this message.
+     *
+     * @retval kErrorNone    Successfully sent CoAP message.
+     * @retval kErrorNoBufs  Insufficient buffers available to send the CoAP message.
+     *
+     */
+    Error SendMessage(Message &aMessage, const Ip6::MessageInfo &aMessageInfo, const TxParameters &aTxParameters);
+    /**
      * This method sends a CoAP message with default transmission parameters.
      *
-     * If a response for a request is expected, respective function and context information should be provided.
-     * If no response is expected, these arguments should be `nullptr` pointers.
-     * If Message Id was not set in the header (equal to 0), this function will assign unique Message Id to the message.
+     * If Message ID was not set in the header (equal to 0), this method will assign unique Message ID to the message.
      *
      * @param[in]  aMessage      A reference to the message to send.
      * @param[in]  aMessageInfo  A reference to the message info associated with @p aMessage.
@@ -622,8 +639,22 @@
      */
     Error SendMessage(Message                &aMessage,
                       const Ip6::MessageInfo &aMessageInfo,
-                      ResponseHandler         aHandler = nullptr,
-                      void                   *aContext = nullptr);
+                      ResponseHandler         aHandler,
+                      void                   *aContext);
+
+    /**
+     * This method sends a CoAP message with default transmission parameters.
+     *
+     * If Message ID was not set in the header (equal to 0), this method will assign unique Message ID to the message.
+     *
+     * @param[in]  aMessage      A reference to the message to send.
+     * @param[in]  aMessageInfo  A reference to the message info associated with @p aMessage.
+     *
+     * @retval kErrorNone    Successfully sent CoAP message.
+     * @retval kErrorNoBufs  Insufficient buffers available to send the CoAP response.
+     *
+     */
+    Error SendMessage(Message &aMessage, const Ip6::MessageInfo &aMessageInfo);
 
     /**
      * This method sends a CoAP reset message.
@@ -677,7 +708,20 @@
      * @retval kErrorInvalidArgs   The @p aRequest header is not of confirmable type.
      *
      */
-    Error SendEmptyAck(const Message &aRequest, const Ip6::MessageInfo &aMessageInfo, Code aCode = kCodeChanged);
+    Error SendEmptyAck(const Message &aRequest, const Ip6::MessageInfo &aMessageInfo, Code aCode);
+
+    /**
+     * This method sends a CoAP ACK message on which a dummy CoAP response is piggybacked.
+     *
+     * @param[in]  aRequest        A reference to the CoAP Message that was used in CoAP request.
+     * @param[in]  aMessageInfo    The message info corresponding to the CoAP request.
+     *
+     * @retval kErrorNone          Successfully enqueued the CoAP response message.
+     * @retval kErrorNoBufs        Insufficient buffers available to send the CoAP response.
+     * @retval kErrorInvalidArgs   The @p aRequest header is not of confirmable type.
+     *
+     */
+    Error SendEmptyAck(const Message &aRequest, const Ip6::MessageInfo &aMessageInfo);
 
     /**
      * This method sends a header-only CoAP message to indicate no resource matched for the request.
diff --git a/src/core/coap/coap_message.hpp b/src/core/coap/coap_message.hpp
index b5bf831..40c9c97 100644
--- a/src/core/coap/coap_message.hpp
+++ b/src/core/coap/coap_message.hpp
@@ -634,7 +634,7 @@
     void SetBlockWiseBlockNumber(uint32_t aBlockNumber) { GetHelpData().mBlockWiseData.mBlockNumber = aBlockNumber; }
 
     /**
-     * This method sets the More Blocks falg in the message HelpData.
+     * This method sets the More Blocks flag in the message HelpData.
      *
      * @param[in]   aMoreBlocks    TRUE or FALSE.
      *
diff --git a/src/core/common/heap_data.hpp b/src/core/common/heap_data.hpp
index 97c12a3..20545da 100644
--- a/src/core/common/heap_data.hpp
+++ b/src/core/common/heap_data.hpp
@@ -94,7 +94,7 @@
     /**
      * This method returns the `Heap::Data` length.
      *
-     * @returns The data length (number of bytes) or zero if the `HeadpData` is null.
+     * @returns The data length (number of bytes) or zero if the `HeapData` is null.
      *
      */
     uint16_t GetLength(void) const { return mData.GetLength(); }
diff --git a/src/core/common/instance.cpp b/src/core/common/instance.cpp
index 384843b..10d545d 100644
--- a/src/core/common/instance.cpp
+++ b/src/core/common/instance.cpp
@@ -136,8 +136,9 @@
     , mNetworkDataPublisher(*this)
 #endif
     , mNetworkDataServiceManager(*this)
-#if OPENTHREAD_FTD || OPENTHREAD_CONFIG_TMF_NETWORK_DIAG_MTD_ENABLE
-    , mNetworkDiagnostic(*this)
+    , mNetworkDiagnosticServer(*this)
+#if OPENTHREAD_CONFIG_TMF_NETDIAG_CLIENT_ENABLE
+    , mNetworkDiagnosticClient(*this)
 #endif
 #if OPENTHREAD_CONFIG_BORDER_AGENT_ENABLE
     , mBorderAgent(*this)
@@ -188,8 +189,11 @@
 #if OPENTHREAD_CONFIG_TIME_SYNC_ENABLE
     , mTimeSync(*this)
 #endif
-#if OPENTHREAD_CONFIG_MLE_LINK_METRICS_INITIATOR_ENABLE || OPENTHREAD_CONFIG_MLE_LINK_METRICS_SUBJECT_ENABLE
-    , mLinkMetrics(*this)
+#if OPENTHREAD_CONFIG_MLE_LINK_METRICS_INITIATOR_ENABLE
+    , mInitiator(*this)
+#endif
+#if OPENTHREAD_CONFIG_MLE_LINK_METRICS_SUBJECT_ENABLE
+    , mSubject(*this)
 #endif
 #if OPENTHREAD_CONFIG_COAP_API_ENABLE
     , mApplicationCoap(*this)
@@ -241,6 +245,7 @@
     , mPowerCalibration(*this)
 #endif
     , mIsInitialized(false)
+    , mId(Random::NonCrypto::GetUint32())
 {
 }
 
@@ -344,6 +349,10 @@
     IgnoreError(otIp6SetEnabled(this, false));
     IgnoreError(otLinkSetEnabled(this, false));
 
+#if OPENTHREAD_CONFIG_PLATFORM_KEY_REFERENCES_ENABLE
+    Get<KeyManager>().DestroyTemporaryKeys();
+#endif
+
     Get<Settings>().Deinit();
 #endif
 
@@ -368,6 +377,10 @@
 void Instance::FactoryReset(void)
 {
     Get<Settings>().Wipe();
+#if OPENTHREAD_CONFIG_PLATFORM_KEY_REFERENCES_ENABLE
+    Get<KeyManager>().DestroyTemporaryKeys();
+    Get<KeyManager>().DestroyPersistentKeys();
+#endif
     otPlatReset(this);
 }
 
@@ -377,6 +390,10 @@
 
     VerifyOrExit(Get<Mle::MleRouter>().IsDisabled(), error = kErrorInvalidState);
     Get<Settings>().Wipe();
+#if OPENTHREAD_CONFIG_PLATFORM_KEY_REFERENCES_ENABLE
+    Get<KeyManager>().DestroyTemporaryKeys();
+    Get<KeyManager>().DestroyPersistentKeys();
+#endif
 
 exit:
     return error;
diff --git a/src/core/common/instance.hpp b/src/core/common/instance.hpp
index 14d21ca..3a5f4fa 100644
--- a/src/core/common/instance.hpp
+++ b/src/core/common/instance.hpp
@@ -208,6 +208,17 @@
 #endif
 
     /**
+     * Gets the instance identifier.
+     *
+     * The instance identifier is set to a random value when the instance is constructed, and then its value will not
+     * change after initialization.
+     *
+     * @returns The instance identifier.
+     *
+     */
+    uint32_t GetId(void) const { return mId; }
+
+    /**
      * This method indicates whether or not the instance is valid/initialized and not yet finalized.
      *
      * @returns TRUE if the instance is valid/initialized, FALSE otherwise.
@@ -495,8 +506,9 @@
 
     NetworkData::Service::Manager mNetworkDataServiceManager;
 
-#if OPENTHREAD_FTD || OPENTHREAD_CONFIG_TMF_NETWORK_DIAG_MTD_ENABLE
-    NetworkDiagnostic::NetworkDiagnostic mNetworkDiagnostic;
+    NetworkDiagnostic::Server mNetworkDiagnosticServer;
+#if OPENTHREAD_CONFIG_TMF_NETDIAG_CLIENT_ENABLE
+    NetworkDiagnostic::Client mNetworkDiagnosticClient;
 #endif
 
 #if OPENTHREAD_CONFIG_BORDER_AGENT_ENABLE
@@ -562,8 +574,12 @@
     TimeSync mTimeSync;
 #endif
 
-#if OPENTHREAD_CONFIG_MLE_LINK_METRICS_INITIATOR_ENABLE || OPENTHREAD_CONFIG_MLE_LINK_METRICS_SUBJECT_ENABLE
-    LinkMetrics::LinkMetrics mLinkMetrics;
+#if OPENTHREAD_CONFIG_MLE_LINK_METRICS_INITIATOR_ENABLE
+    LinkMetrics::Initiator mInitiator;
+#endif
+
+#if OPENTHREAD_CONFIG_MLE_LINK_METRICS_SUBJECT_ENABLE
+    LinkMetrics::Subject mSubject;
 #endif
 
 #if OPENTHREAD_CONFIG_COAP_API_ENABLE
@@ -640,6 +656,8 @@
 #if OPENTHREAD_CONFIG_REFERENCE_DEVICE_ENABLE && (OPENTHREAD_FTD || OPENTHREAD_MTD)
     static bool sDnsNameCompressionEnabled;
 #endif
+
+    uint32_t mId;
 };
 
 DefineCoreType(otInstance, Instance);
@@ -824,8 +842,10 @@
 template <> inline Dns::Dso &Instance::Get(void) { return mDnsDso; }
 #endif
 
-#if OPENTHREAD_FTD || OPENTHREAD_CONFIG_TMF_NETWORK_DIAG_MTD_ENABLE
-template <> inline NetworkDiagnostic::NetworkDiagnostic &Instance::Get(void) { return mNetworkDiagnostic; }
+template <> inline NetworkDiagnostic::Server &Instance::Get(void) { return mNetworkDiagnosticServer; }
+
+#if OPENTHREAD_CONFIG_TMF_NETDIAG_CLIENT_ENABLE
+template <> inline NetworkDiagnostic::Client &Instance::Get(void) { return mNetworkDiagnosticClient; }
 #endif
 
 #if OPENTHREAD_CONFIG_DHCP6_CLIENT_ENABLE
@@ -927,8 +947,12 @@
 template <> inline DuaManager &Instance::Get(void) { return mDuaManager; }
 #endif
 
-#if OPENTHREAD_CONFIG_MLE_LINK_METRICS_INITIATOR_ENABLE || OPENTHREAD_CONFIG_MLE_LINK_METRICS_SUBJECT_ENABLE
-template <> inline LinkMetrics::LinkMetrics &Instance::Get(void) { return mLinkMetrics; }
+#if OPENTHREAD_CONFIG_MLE_LINK_METRICS_INITIATOR_ENABLE
+template <> inline LinkMetrics::Initiator &Instance::Get(void) { return mInitiator; }
+#endif
+
+#if OPENTHREAD_CONFIG_MLE_LINK_METRICS_SUBJECT_ENABLE
+template <> inline LinkMetrics::Subject &Instance::Get(void) { return mSubject; }
 #endif
 
 #endif // (OPENTHREAD_CONFIG_THREAD_VERSION >= OT_THREAD_VERSION_1_2)
diff --git a/src/core/common/log.cpp b/src/core/common/log.cpp
index af1e6da..04c74bb 100644
--- a/src/core/common/log.cpp
+++ b/src/core/common/log.cpp
@@ -98,7 +98,7 @@
     static_assert(sizeof(kModuleNamePadding) == kMaxLogModuleNameLength + 1, "Padding string is not correct");
 
 #if OPENTHREAD_CONFIG_LOG_PREPEND_UPTIME
-    ot::Uptime::UptimeToString(ot::Instance::Get().Get<ot::Uptime>().GetUptime(), logString);
+    ot::Uptime::UptimeToString(ot::Instance::Get().Get<ot::Uptime>().GetUptime(), logString, /* aInlcudeMsec */ true);
     logString.Append(" ");
 #endif
 
diff --git a/src/core/common/message.cpp b/src/core/common/message.cpp
index 1cdd339..e7727e2 100644
--- a/src/core/common/message.cpp
+++ b/src/core/common/message.cpp
@@ -98,6 +98,13 @@
     return message;
 }
 
+Message *MessagePool::Allocate(Message::Type aType) { return Allocate(aType, 0, Message::Settings::GetDefault()); }
+
+Message *MessagePool::Allocate(Message::Type aType, uint16_t aReserveHeader)
+{
+    return Allocate(aType, aReserveHeader, Message::Settings::GetDefault());
+}
+
 void MessagePool::Free(Message *aMessage)
 {
     OT_ASSERT(aMessage->Next() == nullptr && aMessage->Prev() == nullptr);
diff --git a/src/core/common/message.hpp b/src/core/common/message.hpp
index 757d8a8..a1bf2a8 100644
--- a/src/core/common/message.hpp
+++ b/src/core/common/message.hpp
@@ -393,7 +393,7 @@
         /**
          * This static method converts a pointer to an `otMessageSettings` to a `Settings`.
          *
-         * @param[in] aSettings  A pointer to `otMessageSettings` to covert from.
+         * @param[in] aSettings  A pointer to `otMessageSettings` to convert from.
          *                       If it is `nullptr`, then the default settings `GetDefault()` will be used.
          *
          * @returns A reference to the converted `Settings` or the default if @p aSettings is `nullptr`.
@@ -1706,9 +1706,28 @@
      * @returns A pointer to the message or `nullptr` if no message buffers are available.
      *
      */
-    Message *Allocate(Message::Type            aType,
-                      uint16_t                 aReserveHeader = 0,
-                      const Message::Settings &aSettings      = Message::Settings::GetDefault());
+    Message *Allocate(Message::Type aType, uint16_t aReserveHeader, const Message::Settings &aSettings);
+
+    /**
+     * This method allocates a new message of a given type using default settings.
+     *
+     * @param[in]  aType           The message type.
+     *
+     * @returns A pointer to the message or `nullptr` if no message buffers are available.
+     *
+     */
+    Message *Allocate(Message::Type aType);
+
+    /**
+     * This method allocates a new message with a given type and reserved length using default settings.
+     *
+     * @param[in]  aType           The message type.
+     * @param[in]  aReserveHeader  The number of header bytes to reserve.
+     *
+     * @returns A pointer to the message or `nullptr` if no message buffers are available.
+     *
+     */
+    Message *Allocate(Message::Type aType, uint16_t aReserveHeader);
 
     /**
      * This method is used to free a message and return all message buffers to the buffer pool.
diff --git a/src/core/common/new.hpp b/src/core/common/new.hpp
index b1eb950..76d2f5e 100644
--- a/src/core/common/new.hpp
+++ b/src/core/common/new.hpp
@@ -31,8 +31,8 @@
  *   This file defines the new operator used by OpenThread.
  */
 
-#ifndef NEW_HPP_
-#define NEW_HPP_
+#ifndef OT_CORE_COMMON_NEW_HPP_
+#define OT_CORE_COMMON_NEW_HPP_
 
 #include "openthread-core-config.h"
 
@@ -42,4 +42,4 @@
 
 inline void *operator new(size_t, void *p) throw() { return p; }
 
-#endif // NEW_HPP_
+#endif // OT_CORE_COMMON_NEW_HPP_
diff --git a/src/core/common/notifier.cpp b/src/core/common/notifier.cpp
index 4cc5a96..706350b 100644
--- a/src/core/common/notifier.cpp
+++ b/src/core/common/notifier.cpp
@@ -271,6 +271,7 @@
         "JoinerState",       // kEventJoinerStateChanged               (1 << 27)
         "ActDset",           // kEventActiveDatasetChanged             (1 << 28)
         "PndDset",           // kEventPendingDatasetChanged            (1 << 29)
+        "Nat64",             // kEventNat64TranslatorStateChanged      (1 << 30)
     };
 
     for (uint8_t index = 0; index < GetArrayLength(kEventStrings); index++)
diff --git a/src/core/common/pool.hpp b/src/core/common/pool.hpp
index 279655f..4a0f0e5 100644
--- a/src/core/common/pool.hpp
+++ b/src/core/common/pool.hpp
@@ -82,7 +82,7 @@
      * This constructor initializes the pool.
      *
      * This constructor version requires the `Type` class to provide method `void Init(Instance &)` to initialize
-     * each `Type` entry object. This can be realized by the `Type` class inheriting from `InstaceLocatorInit()`.
+     * each `Type` entry object. This can be realized by the `Type` class inheriting from `InstanceLocatorInit()`.
      *
      * @param[in] aInstance   A reference to the OpenThread instance.
      *
diff --git a/src/core/common/preference.hpp b/src/core/common/preference.hpp
index a4e2651..cf0657d 100644
--- a/src/core/common/preference.hpp
+++ b/src/core/common/preference.hpp
@@ -87,7 +87,7 @@
 
     /**
      * This static method indicates whether a given `int8_t` preference value is valid, i.e., whether it has of the
-     * three values `kHigh`, `kMediumm`, or `kLow`.
+     * three values `kHigh`, `kMedium`, or `kLow`.
      *
      * @param[in] aPrf  The signed preference value to check.
      *
diff --git a/src/core/common/settings.cpp b/src/core/common/settings.cpp
index d1a072a..c9b6f3a 100644
--- a/src/core/common/settings.cpp
+++ b/src/core/common/settings.cpp
@@ -106,6 +106,17 @@
 }
 #endif
 
+#if OPENTHREAD_CONFIG_BORDER_AGENT_ID_ENABLE
+void SettingsBase::BorderAgentId::Log(Action aAction) const
+{
+    char         buffer[sizeof(BorderAgentId) * 2 + 1];
+    StringWriter sw(buffer, sizeof(buffer));
+
+    sw.AppendHexBytes(GetId().mId, sizeof(BorderAgentId));
+    LogInfo("%s BorderAgentId {id:%s}", ActionToString(aAction), buffer);
+}
+#endif // OPENTHREAD_CONFIG_BORDER_AGENT_ID_ENABLE
+
 #endif // OT_SHOULD_LOG_AT(OT_LOG_LEVEL_INFO)
 
 #if OT_SHOULD_LOG_AT(OT_LOG_LEVEL_INFO)
@@ -157,7 +168,8 @@
         "SrpServerInfo",     // (13) kKeySrpServerInfo
         "",                  // (14) Removed (previously NAT64 prefix)
         "BrUlaPrefix",       // (15) kKeyBrUlaPrefix
-        "BrOnLinkPrefixes"   // (16) kKeyBrOnLinkPrefixes
+        "BrOnLinkPrefixes",  // (16) kKeyBrOnLinkPrefixes
+        "BorderAgentId"      // (17) kKeyBorderAgentId
     };
 
     static_assert(1 == kKeyActiveDataset, "kKeyActiveDataset value is incorrect");
@@ -172,8 +184,9 @@
     static_assert(13 == kKeySrpServerInfo, "kKeySrpServerInfo value is incorrect");
     static_assert(15 == kKeyBrUlaPrefix, "kKeyBrUlaPrefix value is incorrect");
     static_assert(16 == kKeyBrOnLinkPrefixes, "kKeyBrOnLinkPrefixes is incorrect");
+    static_assert(17 == kKeyBorderAgentId, "kKeyBorderAgentId is incorrect");
 
-    static_assert(kLastKey == kKeyBrOnLinkPrefixes, "kLastKey is not valid");
+    static_assert(kLastKey == kKeyBorderAgentId, "kLastKey is not valid");
 
     OT_ASSERT(aKey <= kLastKey);
 
@@ -518,6 +531,12 @@
             break;
 #endif
 
+#if OPENTHREAD_CONFIG_BORDER_AGENT_ID_ENABLE
+        case kKeyBorderAgentId:
+            reinterpret_cast<const BorderAgentId *>(aValue)->Log(aAction);
+            break;
+#endif
+
         default:
             // For any other keys, we do not want to include the value
             // in the log, so even if it is given we set `aValue` to
diff --git a/src/core/common/settings.hpp b/src/core/common/settings.hpp
index 10e1487..ec32e27 100644
--- a/src/core/common/settings.hpp
+++ b/src/core/common/settings.hpp
@@ -47,6 +47,7 @@
 #include "common/settings_driver.hpp"
 #include "crypto/ecdsa.hpp"
 #include "mac/mac_types.hpp"
+#include "meshcop/border_agent.hpp"
 #include "meshcop/dataset.hpp"
 #include "net/ip6_address.hpp"
 #include "thread/version.hpp"
@@ -120,9 +121,10 @@
         kKeySrpServerInfo     = OT_SETTINGS_KEY_SRP_SERVER_INFO,
         kKeyBrUlaPrefix       = OT_SETTINGS_KEY_BR_ULA_PREFIX,
         kKeyBrOnLinkPrefixes  = OT_SETTINGS_KEY_BR_ON_LINK_PREFIXES,
+        kKeyBorderAgentId     = OT_SETTINGS_KEY_BORDER_AGENT_ID,
     };
 
-    static constexpr Key kLastKey = kKeyBrOnLinkPrefixes; ///< The last (numerically) enumerator value in `Key`.
+    static constexpr Key kLastKey = kKeyBorderAgentId; ///< The last (numerically) enumerator value in `Key`.
 
     static_assert(static_cast<uint16_t>(kLastKey) < static_cast<uint16_t>(OT_SETTINGS_KEY_VENDOR_RESERVED_MIN),
                   "Core settings keys overlap with vendor reserved keys");
@@ -765,6 +767,54 @@
     } OT_TOOL_PACKED_END;
 #endif // OPENTHREAD_CONFIG_SRP_SERVER_ENABLE && OPENTHREAD_CONFIG_SRP_SERVER_PORT_SWITCH_ENABLE
 
+#if OPENTHREAD_CONFIG_BORDER_AGENT_ID_ENABLE
+    /**
+     * This structure represents the Border Agent ID.
+     *
+     */
+    OT_TOOL_PACKED_BEGIN
+    class BorderAgentId
+    {
+        friend class Settings;
+
+    public:
+        static constexpr Key kKey = kKeyBorderAgentId; ///< The associated key.
+
+        /**
+         * This method initializes the `BorderAgentId` object.
+         *
+         */
+        void Init(void) { mId = {}; }
+
+        /**
+         * This method returns the Border Agent ID.
+         *
+         * @returns The Border Agent ID.
+         *
+         */
+        const MeshCoP::BorderAgent::Id &GetId(void) const { return mId; }
+
+        /**
+         * This method returns the Border Agent ID.
+         *
+         * @returns The Border Agent ID.
+         *
+         */
+        MeshCoP::BorderAgent::Id &GetId(void) { return mId; }
+
+        /**
+         * This method sets the Border Agent ID.
+         *
+         */
+        void SetId(const MeshCoP::BorderAgent::Id &aId) { mId = aId; }
+
+    private:
+        void Log(Action aAction) const;
+
+        MeshCoP::BorderAgent::Id mId;
+    } OT_TOOL_PACKED_END;
+#endif // OPENTHREAD_CONFIG_BORDER_AGENT_ID_ENABLE
+
 protected:
     explicit SettingsBase(Instance &aInstance)
         : InstanceLocator(aInstance)
diff --git a/src/core/common/string.cpp b/src/core/common/string.cpp
index bc3069c..c7aa721 100644
--- a/src/core/common/string.cpp
+++ b/src/core/common/string.cpp
@@ -160,6 +160,32 @@
     return Match(aFirstString, aSecondString, aMode) == kFullMatch;
 }
 
+Error StringParseUint8(const char *&aString, uint8_t &aUint8)
+{
+    return StringParseUint8(aString, aUint8, NumericLimits<uint8_t>::kMax);
+}
+
+Error StringParseUint8(const char *&aString, uint8_t &aUint8, uint8_t aMaxValue)
+{
+    Error       error = kErrorParse;
+    const char *cur   = aString;
+    uint16_t    value = 0;
+
+    for (; (*cur >= '0') && (*cur <= '9'); cur++)
+    {
+        value *= 10;
+        value += static_cast<uint8_t>(*cur - '0');
+        VerifyOrExit(value <= aMaxValue, error = kErrorParse);
+        error = kErrorNone;
+    }
+
+    aString = cur;
+    aUint8  = static_cast<uint8_t>(value);
+
+exit:
+    return error;
+}
+
 void StringConvertToLowercase(char *aString)
 {
     for (; *aString != kNullChar; aString++)
diff --git a/src/core/common/string.hpp b/src/core/common/string.hpp
index f80454d..fbbaca7 100644
--- a/src/core/common/string.hpp
+++ b/src/core/common/string.hpp
@@ -144,7 +144,7 @@
 bool StringEndsWith(const char *aString, const char *aSubString, StringMatchMode aMode = kStringExactMatch);
 
 /**
- * This method checks whether or not two null-terminated strings match.
+ * This function checks whether or not two null-terminated strings match.
  *
  * @param[in] aFirstString   A pointer to the first string.
  * @param[in] aSecondString  A pointer to the second string.
@@ -157,6 +157,45 @@
 bool StringMatch(const char *aFirstString, const char *aSecondString, StringMatchMode aMode = kStringExactMatch);
 
 /**
+ * This function parses a decimal number from a string as `uint8_t` and skips over the parsed characters.
+ *
+ * If the string does not start with a digit, `kErrorParse` is returned.
+ *
+ * All the digit characters in the string are parsed until reaching a non-digit character. The pointer `aString` is
+ * updated to point to the first non-digit character after the parsed digits.
+ *
+ * If the parsed number value is larger than @p aMaxValue, `kErrorParse` is returned.
+ *
+ * @param[in,out] aString    A reference to a pointer to string to parse.
+ * @param[out]    aUint8     A reference to return the parsed value.
+ * @param[in]     aMaxValue  Maximum allowed value for the parsed number.
+ *
+ * @retval kErrorNone   Successfully parsed the number from string. @p aString and @p aUint8 are updated.
+ * @retval kErrorParse  Failed to parse the number from @p aString, or parsed number is larger than @p aMaxValue.
+ *
+ */
+Error StringParseUint8(const char *&aString, uint8_t &aUint8, uint8_t aMaxValue);
+
+/**
+ * This function parses a decimal number from a string as `uint8_t` and skips over the parsed characters.
+ *
+ * If the string does not start with a digit, `kErrorParse` is returned.
+ *
+ * All the digit characters in the string are parsed until reaching a non-digit character. The pointer `aString` is
+ * updated to point to the first non-digit character after the parsed digits.
+ *
+ * If the parsed number value is larger than maximum `uint8_t` value, `kErrorParse` is returned.
+ *
+ * @param[in,out] aString    A reference to a pointer to string to parse.
+ * @param[out]    aUint8     A reference to return the parsed value.
+ *
+ * @retval kErrorNone   Successfully parsed the number from string. @p aString and @p aUint8 are updated.
+ * @retval kErrorParse  Failed to parse the number from @p aString, or parsed number is out of range.
+ *
+ */
+Error StringParseUint8(const char *&aString, uint8_t &aUint8);
+
+/**
  * This function converts all uppercase letter characters in a given string to lowercase.
  *
  * @param[in,out] aString   A pointer to the string to convert.
diff --git a/src/core/common/trickle_timer.cpp b/src/core/common/trickle_timer.cpp
index fe84d8a..7258cd2 100644
--- a/src/core/common/trickle_timer.cpp
+++ b/src/core/common/trickle_timer.cpp
@@ -158,7 +158,7 @@
             }
 
             StartNewInterval();
-            ExitNow(); // Exit so to not call `mHanlder`
+            ExitNow(); // Exit so to not call `mHandler`
         }
 
         break;
diff --git a/src/core/common/uptime.cpp b/src/core/common/uptime.cpp
index 669b1ff..4cf0a34 100644
--- a/src/core/common/uptime.cpp
+++ b/src/core/common/uptime.cpp
@@ -85,7 +85,7 @@
 {
     StringWriter writer(aBuffer, aSize);
 
-    UptimeToString(GetUptime(), writer);
+    UptimeToString(GetUptime(), writer, /* aIncludeMsec */ true);
 }
 
 void Uptime::HandleTimer(void)
@@ -110,7 +110,7 @@
     return static_cast<uint16_t>(quotient);
 }
 
-void Uptime::UptimeToString(uint64_t aUptime, StringWriter &aWriter)
+void Uptime::UptimeToString(uint64_t aUptime, StringWriter &aWriter, bool aIncludeMsec)
 {
     uint64_t days = aUptime / Time::kOneDayInMsec;
     uint32_t remainder;
@@ -129,7 +129,12 @@
     minutes   = DivideAndGetRemainder(remainder, Time::kOneMinuteInMsec);
     seconds   = DivideAndGetRemainder(remainder, Time::kOneSecondInMsec);
 
-    aWriter.Append("%02u:%02u:%02u.%03u", hours, minutes, seconds, static_cast<uint16_t>(remainder));
+    aWriter.Append("%02u:%02u:%02u", hours, minutes, seconds);
+
+    if (aIncludeMsec)
+    {
+        aWriter.Append(".%03u", static_cast<uint16_t>(remainder));
+    }
 }
 
 } // namespace ot
diff --git a/src/core/common/uptime.hpp b/src/core/common/uptime.hpp
index f3c33e9..041a7a5 100644
--- a/src/core/common/uptime.hpp
+++ b/src/core/common/uptime.hpp
@@ -36,6 +36,10 @@
 
 #include "openthread-core-config.h"
 
+#if !OPENTHREAD_CONFIG_UPTIME_ENABLE && OPENTHREAD_FTD
+#error "OPENTHREAD_CONFIG_UPTIME_ENABLE is required for FTD"
+#endif
+
 #if OPENTHREAD_CONFIG_UPTIME_ENABLE
 
 #include "common/locator.hpp"
@@ -90,13 +94,38 @@
      * This method converts an uptime value (number of milliseconds) to a human-readable string.
      *
      * The string follows the format "<hh>:<mm>:<ss>.<mmmm>" for hours, minutes, seconds and millisecond (if uptime is
-     * shorter than one day) or "<dd>d.<hh>:<mm>:<ss>.<mmmm>" (if longer than a day).
+     * shorter than one day) or "<dd>d.<hh>:<mm>:<ss>.<mmmm>" (if longer than a day). @p aIncludeMsec can be used
+     * to determine whether `.<mmm>` milliseconds is included or omitted in the resulting string.
      *
-     * @param[in]     aUptime  The uptime to convert.
-     * @param[in,out] aWriter  A `StringWriter` to append the converted string to.
+     * @param[in]     aUptime        The uptime to convert.
+     * @param[in,out] aWriter        A `StringWriter` to append the converted string to.
+     * @param[in]     aIncludeMsec   Whether to include `.<mmm>` milliseconds in the string.
      *
      */
-    static void UptimeToString(uint64_t aUptime, StringWriter &aWriter);
+    static void UptimeToString(uint64_t aUptime, StringWriter &aWriter, bool aIncludeMsec);
+
+    /**
+     * This static method converts a given uptime as number of milliseconds to number of seconds.
+     *
+     * @param[in] aUptimeInMilliseconds    Uptime in milliseconds (as `uint64_t`).
+     *
+     * @returns The converted @p aUptimeInMilliseconds to seconds (as `uint32_t`).
+     *
+     */
+    static uint32_t MsecToSec(uint64_t aUptimeInMilliseconds)
+    {
+        return static_cast<uint32_t>(aUptimeInMilliseconds / 1000u);
+    }
+
+    /**
+     * This static method converts a given uptime as number of seconds to number of milliseconds.
+     *
+     * @param[in] aUptimeInSeconds    Uptime in seconds (as `uint32_t`).
+     *
+     * @returns The converted @p aUptimeInSeconds to milliseconds (as `uint64_t`).
+     *
+     */
+    static uint64_t SecToMsec(uint32_t aUptimeInSeconds) { return static_cast<uint64_t>(aUptimeInSeconds) * 1000u; }
 
 private:
     static constexpr uint32_t kTimerInterval = (1 << 30);
diff --git a/src/core/config/border_agent.h b/src/core/config/border_agent.h
index 39d7fe7..7e0abc1 100644
--- a/src/core/config/border_agent.h
+++ b/src/core/config/border_agent.h
@@ -55,4 +55,14 @@
 #define OPENTHREAD_CONFIG_BORDER_AGENT_UDP_PORT 0
 #endif
 
+/**
+ * @def OPENTHREAD_CONFIG_BORDER_AGENT_ID_ENABLE
+ *
+ * Define ro 1 to enable Border Agent ID support.
+ *
+ */
+#ifndef OPENTHREAD_CONFIG_BORDER_AGENT_ID_ENABLE
+#define OPENTHREAD_CONFIG_BORDER_AGENT_ID_ENABLE 0
+#endif
+
 #endif // CONFIG_BORDER_AGENT_H_
diff --git a/src/core/config/dns_client.h b/src/core/config/dns_client.h
index 99a6520..d727261 100644
--- a/src/core/config/dns_client.h
+++ b/src/core/config/dns_client.h
@@ -152,6 +152,16 @@
 #endif
 
 /**
+ * @def OPENTHREAD_CONFIG_DNS_CLIENT_DEFAULT_SERVICE_MODE
+ *
+ * Specifies the default `otDnsServiceMode` to use. The value MUST be from `otDnsServiceMode` enumeration.
+ *
+ */
+#ifndef OPENTHREAD_CONFIG_DNS_CLIENT_DEFAULT_SERVICE_MODE
+#define OPENTHREAD_CONFIG_DNS_CLIENT_DEFAULT_SERVICE_MODE OT_DNS_SERVICE_MODE_SRV_TXT_OPTIMIZE
+#endif
+
+/**
  * @def OPENTHREAD_CONFIG_DNS_CLIENT_OVER_TCP_ENABLE
  *
  * Enables support for sending DNS Queries over TCP.
diff --git a/src/core/config/mac.h b/src/core/config/mac.h
index 0358b22..2694e0d 100644
--- a/src/core/config/mac.h
+++ b/src/core/config/mac.h
@@ -489,16 +489,27 @@
 #endif
 
 /**
- * @def OPENTHREAD_CONFIG_CSL_MIN_RECEIVE_ON
+ * @def OPENTHREAD_CONFIG_MIN_RECEIVE_ON_AHEAD
  *
- * The minimum CSL receive window (in microseconds) required to receive an IEEE 802.15.4 frame.
- * - Maximum frame size with preamble: 6*2+127*2 symbols
- * - AIFS: 12 symbols
- * - Maximum ACK size with preamble: 6*2+33*2 symbols
+ * The minimum time (in microseconds) before the MHR start that the radio should be in receive state and ready to
+ * properly receive in order to properly receive any IEEE 802.15.4 frame. Defaults to the duration of SHR + PHR.
  *
  */
-#ifndef OPENTHREAD_CONFIG_CSL_MIN_RECEIVE_ON
-#define OPENTHREAD_CONFIG_CSL_MIN_RECEIVE_ON 356 * 16
+#ifndef OPENTHREAD_CONFIG_MIN_RECEIVE_ON_AHEAD
+#define OPENTHREAD_CONFIG_MIN_RECEIVE_ON_AHEAD (6 * 32)
+#endif
+
+/**
+ * @def OPENTHREAD_CONFIG_MIN_RECEIVE_ON_AFTER
+ *
+ * The minimum time (in microseconds) after the MHR start that the radio should be in receive state in order
+ * to properly receive any IEEE 802.15.4 frame. Defaults to the duration of a maximum size frame, plus AIFS,
+ * plus the duration of maximum enh-ack frame. Platforms are encouraged to improve this value for energy
+ * efficiency purposes.
+ *
+ */
+#ifndef OPENTHREAD_CONFIG_MIN_RECEIVE_ON_AFTER
+#define OPENTHREAD_CONFIG_MIN_RECEIVE_ON_AFTER ((127 + 6 + 39) * 32)
 #endif
 
 /**
diff --git a/src/core/config/misc.h b/src/core/config/misc.h
index 918531c..c857850 100644
--- a/src/core/config/misc.h
+++ b/src/core/config/misc.h
@@ -103,7 +103,7 @@
 /**
  * @def OPENTHREAD_CONFIG_DETERMINISTIC_ECDSA_ENABLE
  *
- * Define to 1 to generate ECDSA signatures determinsitically
+ * Define to 1 to generate ECDSA signatures deterministically
  * according to RFC 6979 instead of randomly.
  *
  */
@@ -118,7 +118,7 @@
  *
  */
 #ifndef OPENTHREAD_CONFIG_UPTIME_ENABLE
-#define OPENTHREAD_CONFIG_UPTIME_ENABLE 0
+#define OPENTHREAD_CONFIG_UPTIME_ENABLE OPENTHREAD_FTD
 #endif
 
 /**
@@ -194,7 +194,7 @@
  * to that on 32bit system. As a result, the first message always have some
  * bytes left for small packets.
  *
- * Some configuration options can increase the buffer size requirments, including
+ * Some configuration options can increase the buffer size requirements, including
  * OPENTHREAD_CONFIG_MLE_MAX_CHILDREN and OPENTHREAD_CONFIG_COAP_BLOCKWISE_TRANSFER_ENABLE.
  *
  */
@@ -271,9 +271,9 @@
 /**
  * @def OPENTHREAD_CONFIG_ENABLE_BUILTIN_MBEDTLS
  *
- * Define as 1 to enable bultin-mbedtls.
+ * Define as 1 to enable builtin-mbedtls.
  *
- * Note that the OPENTHREAD_CONFIG_ENABLE_BUILTIN_MBEDTLS determines whether to use bultin-mbedtls as well as
+ * Note that the OPENTHREAD_CONFIG_ENABLE_BUILTIN_MBEDTLS determines whether to use builtin-mbedtls as well as
  * whether to manage mbedTLS internally, such as memory allocation and debug.
  *
  */
@@ -284,7 +284,7 @@
 /**
  * @def OPENTHREAD_CONFIG_ENABLE_BUILTIN_MBEDTLS_MANAGEMENT
  *
- * Define as 1 to enable bultin mbedtls management.
+ * Define as 1 to enable builtin mbedtls management.
  *
  * OPENTHREAD_CONFIG_ENABLE_BUILTIN_MBEDTLS_MANAGEMENT determines whether to manage mbedTLS memory
  * allocation and debug config internally.  If not configured, the default is to enable builtin
diff --git a/src/core/config/mle.h b/src/core/config/mle.h
index 431b7e7..57e1e50 100644
--- a/src/core/config/mle.h
+++ b/src/core/config/mle.h
@@ -269,7 +269,19 @@
  *
  */
 #ifndef OPENTHREAD_CONFIG_MLE_INFORM_PREVIOUS_PARENT_ON_REATTACH
-#define OPENTHREAD_CONFIG_MLE_INFORM_PREVIOUS_PARENT_ON_REATTACH 0
+#define OPENTHREAD_CONFIG_MLE_INFORM_PREVIOUS_PARENT_ON_REATTACH 1
+#endif
+
+/**
+ * @def OPENTHREAD_CONFIG_MLE_PARENT_RESPONSE_CALLBACK_API_ENABLE
+ *
+ * Define as 1 to support `otThreadRegisterParentResponseCallback()` API which registers a callback to notify user
+ * of received Parent Response message(s) during attach. This API is mainly intended for debugging and therefore is
+ * disabled by default.
+ *
+ */
+#ifndef OPENTHREAD_CONFIG_MLE_PARENT_RESPONSE_CALLBACK_API_ENABLE
+#define OPENTHREAD_CONFIG_MLE_PARENT_RESPONSE_CALLBACK_API_ENABLE 0
 #endif
 
 /**
diff --git a/src/core/config/network_diagnostic.h b/src/core/config/network_diagnostic.h
new file mode 100644
index 0000000..2aaf260
--- /dev/null
+++ b/src/core/config/network_diagnostic.h
@@ -0,0 +1,85 @@
+/*
+ *  Copyright (c) 2023, The OpenThread Authors.
+ *  All rights reserved.
+ *
+ *  Redistribution and use in source and binary forms, with or without
+ *  modification, are permitted provided that the following conditions are met:
+ *  1. Redistributions of source code must retain the above copyright
+ *     notice, this list of conditions and the following disclaimer.
+ *  2. Redistributions in binary form must reproduce the above copyright
+ *     notice, this list of conditions and the following disclaimer in the
+ *     documentation and/or other materials provided with the distribution.
+ *  3. Neither the name of the copyright holder nor the
+ *     names of its contributors may be used to endorse or promote products
+ *     derived from this software without specific prior written permission.
+ *
+ *  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ *  AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ *  IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ *  ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ *  LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ *  CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ *  SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ *  INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ *  CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ *  ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ *  POSSIBILITY OF SUCH DAMAGE.
+ */
+
+/**
+ * @file
+ *   This file includes compile-time configurations for the Network Diagnostics.
+ *
+ */
+
+#ifndef CONFIG_NETWORK_DIAGNOSTIC_H_
+#define CONFIG_NETWORK_DIAGNOSTIC_H_
+
+/**
+ * @def OPENTHREAD_CONFIG_NET_DIAG_VENDOR_NAME
+ *
+ * Specifies the default Vendor Name string.
+ *
+ */
+#ifndef OPENTHREAD_CONFIG_NET_DIAG_VENDOR_NAME
+#define OPENTHREAD_CONFIG_NET_DIAG_VENDOR_NAME ""
+#endif
+
+/**
+ * @def OPENTHREAD_CONFIG_NET_DIAG_VENDOR_MODEL
+ *
+ * Specifies the default Vendor Model string.
+ *
+ */
+#ifndef OPENTHREAD_CONFIG_NET_DIAG_VENDOR_MODEL
+#define OPENTHREAD_CONFIG_NET_DIAG_VENDOR_MODEL ""
+#endif
+
+/**
+ * @def OPENTHREAD_CONFIG_NET_DIAG_VENDOR_SW_VERSION
+ *
+ * Specifies the default Vendor SW Version string.
+ *
+ */
+#ifndef OPENTHREAD_CONFIG_NET_DIAG_VENDOR_SW_VERSION
+#define OPENTHREAD_CONFIG_NET_DIAG_VENDOR_SW_VERSION ""
+#endif
+
+/**
+ * @def OPENTHREAD_CONFIG_NET_DIAG_VENDOR_INFO_SET_API_ENABLE
+ *
+ * Define as 1 to add APIs to allow Vendor Name, Model, SW Version to change at run-time.
+ *
+ * It is recommended that Vendor Name, Model, and SW Version are set at build time using the OpenThread configurations
+ * `OPENTHREAD_CONFIG_NET_DIAG_VENDOR_*`. This way they are treated as constants and won't consume RAM.
+ *
+ * However, for situations where the OpenThread stack is integrated as a library into different projects/products, this
+ * config can be used to add API to change Vendor Name, Model, and SW Version at run-time. In this case, the strings in
+ * `OPENTHREAD_CONFIG_NET_DIAG_VENDOR_*` are treated as the default values (used when OT stack is initialized).
+ *
+ */
+#ifndef OPENTHREAD_CONFIG_NET_DIAG_VENDOR_INFO_SET_API_ENABLE
+#define OPENTHREAD_CONFIG_NET_DIAG_VENDOR_INFO_SET_API_ENABLE 0
+#endif
+
+#endif // CONFIG_NETWORK_DIAGNOSTIC_H_
diff --git a/src/core/config/openthread-core-config-check.h b/src/core/config/openthread-core-config-check.h
index 9912147..5d26a6e 100644
--- a/src/core/config/openthread-core-config-check.h
+++ b/src/core/config/openthread-core-config-check.h
@@ -65,6 +65,10 @@
 #error "OPENTHREAD_CONFIG_ENABLE_AUTO_START_SUPPORT was removed."
 #endif
 
+#ifdef OPENTHREAD_ENABLE_ANDROID_NDK
+#error "OPENTHREAD_ENABLE_ANDROID_NDK was replaced by OPENTHREAD_CONFIG_ANDROID_NDK_ENABLE."
+#endif
+
 #ifdef OPENTHREAD_ENABLE_CERT_LOG
 #error "OPENTHREAD_ENABLE_CERT_LOG was replaced by OPENTHREAD_CONFIG_REFERENCE_DEVICE_ENABLE."
 #endif
@@ -505,7 +509,7 @@
 
 #ifdef OPENTHREAD_CONFIG_SRP_SERVER_SERVICE_NUMBER
 #error "OPENTHREAD_CONFIG_SRP_SERVER_SERVICE_NUMBER was removed. "\
-       "Service numbers are defined in `network_data_servcie.hpp` per spec"
+       "Service numbers are defined in `network_data_service.hpp` per spec"
 #endif
 
 #ifdef OPENTHREAD_CONFIG_SRP_SERVER_UDP_PORT
@@ -648,4 +652,25 @@
 #error "OPENTHREAD_CONFIG_CHILD_SUPERVISION_MSG_NO_ACK_REQUEST is removed".
 #endif
 
+#ifdef OPENTHREAD_CONFIG_TMF_NETWORK_DIAG_MTD_ENABLE
+#error "OPENTHREAD_CONFIG_TMF_NETWORK_DIAG_MTD_ENABLE is removed. "\
+        "Use OPENTHREAD_CONFIG_TMF_NETDIAG_CLIENT_ENABLE to enable client functionality."\
+        "Netdiag server functionality is always supported."
+#endif
+
+#ifdef OPENTHREAD_CONFIG_PING_SENDER_DEFAULT_INTEVRAL
+#error "OPENTHREAD_CONFIG_PING_SENDER_DEFAULT_INTEVRAL was replaced by "\
+       "OPENTHREAD_CONFIG_PING_SENDER_DEFAULT_INTERVAL."
+#endif
+
+#ifdef OPENTHREAD_CONFIG_SRP_SERVER_DEFAULT_ADDDRESS_MODE
+#error "OPENTHREAD_CONFIG_SRP_SERVER_DEFAULT_ADDDRESS_MODE was replaced by "\
+       "OPENTHREAD_CONFIG_SRP_SERVER_DEFAULT_ADDRESS_MODE."
+#endif
+
+#ifdef OPENTHREAD_CONFIG_CSL_MIN_RECEIVE_ON
+#error "OPENTHREAD_CONFIG_CSL_MIN_RECEIVE_ON was replaced with "\
+        "OPENTHREAD_CONFIG_MIN_RECEIVE_ON_AHEAD and OPENTHREAD_CONFIG_MIN_RECEIVE_ON_AFTER"
+#endif
+
 #endif // OPENTHREAD_CORE_CONFIG_CHECK_H_
diff --git a/src/core/config/ping_sender.h b/src/core/config/ping_sender.h
index 2143389..d655b1d 100644
--- a/src/core/config/ping_sender.h
+++ b/src/core/config/ping_sender.h
@@ -48,13 +48,13 @@
 #endif
 
 /**
- * @def OPENTHREAD_CONFIG_PING_SENDER_DEFAULT_INTEVRAL
+ * @def OPENTHREAD_CONFIG_PING_SENDER_DEFAULT_INTERVAL
  *
  * Specifies the default ping interval (time between sending echo requests) in milliseconds.
  *
  */
-#ifndef OPENTHREAD_CONFIG_PING_SENDER_DEFAULT_INTEVRAL
-#define OPENTHREAD_CONFIG_PING_SENDER_DEFAULT_INTEVRAL 1000
+#ifndef OPENTHREAD_CONFIG_PING_SENDER_DEFAULT_INTERVAL
+#define OPENTHREAD_CONFIG_PING_SENDER_DEFAULT_INTERVAL 1000
 #endif
 
 /**
diff --git a/src/core/config/srp_server.h b/src/core/config/srp_server.h
index aeffbdf..5b76ec3 100644
--- a/src/core/config/srp_server.h
+++ b/src/core/config/srp_server.h
@@ -46,7 +46,7 @@
 #endif
 
 /**
- * @def OPENTHREAD_CONFIG_SRP_SERVER_DEFAULT_ADDDRESS_MODE
+ * @def OPENTHREAD_CONFIG_SRP_SERVER_DEFAULT_ADDRESS_MODE
  *
  * Specifies the default address mode used by the SRP server.
  *
@@ -56,8 +56,8 @@
  * The value of this configuration should be from `otSrpServerAddressMode` enumeration.
  *
  */
-#ifndef OPENTHREAD_CONFIG_SRP_SERVER_DEFAULT_ADDDRESS_MODE
-#define OPENTHREAD_CONFIG_SRP_SERVER_DEFAULT_ADDDRESS_MODE OT_SRP_SERVER_ADDRESS_MODE_UNICAST
+#ifndef OPENTHREAD_CONFIG_SRP_SERVER_DEFAULT_ADDRESS_MODE
+#define OPENTHREAD_CONFIG_SRP_SERVER_DEFAULT_ADDRESS_MODE OT_SRP_SERVER_ADDRESS_MODE_UNICAST
 #endif
 
 /**
diff --git a/src/core/config/tmf.h b/src/core/config/tmf.h
index 0bd1f36..653ed75 100644
--- a/src/core/config/tmf.h
+++ b/src/core/config/tmf.h
@@ -173,13 +173,16 @@
 #endif
 
 /**
- * @def OPENTHREAD_CONFIG_TMF_NETWORK_DIAG_MTD_ENABLE
+ * @def OPENTHREAD_CONFIG_TMF_NETDIAG_CLIENT_ENABLE
  *
- * Define to 1 to enable TMF network diagnostics on MTDs.
+ * Define to 1 to enable TMF network diagnostics client.
+ *
+ * The network diagnostic client add API to send diagnostic requests and queries to other node and process the response.
+ * It is enabled by default on Border Routers.
  *
  */
-#ifndef OPENTHREAD_CONFIG_TMF_NETWORK_DIAG_MTD_ENABLE
-#define OPENTHREAD_CONFIG_TMF_NETWORK_DIAG_MTD_ENABLE 0
+#ifndef OPENTHREAD_CONFIG_TMF_NETDIAG_CLIENT_ENABLE
+#define OPENTHREAD_CONFIG_TMF_NETDIAG_CLIENT_ENABLE OPENTHREAD_CONFIG_BORDER_ROUTING_ENABLE
 #endif
 
 /**
@@ -225,7 +228,7 @@
 /**
  * @def OPENTHREAD_CONFIG_TMF_PROXY_MLR_ENABLE
  *
- * This setting configures the Multicast Listener Registration parent proxing in Thread 1.2.
+ * This setting configures the Multicast Listener Registration parent proxying in Thread 1.2.
  *
  */
 #ifndef OPENTHREAD_CONFIG_TMF_PROXY_MLR_ENABLE
diff --git a/src/core/crypto/crypto_platform.cpp b/src/core/crypto/crypto_platform.cpp
index bfb33ec..a8da82e 100644
--- a/src/core/crypto/crypto_platform.cpp
+++ b/src/core/crypto/crypto_platform.cpp
@@ -662,6 +662,53 @@
 
 #endif // #if !OPENTHREAD_RADIO
 
+#elif OPENTHREAD_CONFIG_CRYPTO_LIB == OPENTHREAD_CONFIG_CRYPTO_LIB_PSA
+
+#if !OPENTHREAD_RADIO
+#if OPENTHREAD_CONFIG_ECDSA_ENABLE
+
+OT_TOOL_WEAK otError otPlatCryptoEcdsaGenerateKey(otPlatCryptoEcdsaKeyPair *aKeyPair)
+{
+    OT_UNUSED_VARIABLE(aKeyPair);
+
+    return OT_ERROR_NOT_CAPABLE;
+}
+
+OT_TOOL_WEAK otError otPlatCryptoEcdsaGetPublicKey(const otPlatCryptoEcdsaKeyPair *aKeyPair,
+                                                   otPlatCryptoEcdsaPublicKey     *aPublicKey)
+{
+    OT_UNUSED_VARIABLE(aKeyPair);
+    OT_UNUSED_VARIABLE(aPublicKey);
+
+    return OT_ERROR_NOT_CAPABLE;
+}
+
+OT_TOOL_WEAK otError otPlatCryptoEcdsaSign(const otPlatCryptoEcdsaKeyPair *aKeyPair,
+                                           const otPlatCryptoSha256Hash   *aHash,
+                                           otPlatCryptoEcdsaSignature     *aSignature)
+{
+    OT_UNUSED_VARIABLE(aKeyPair);
+    OT_UNUSED_VARIABLE(aHash);
+    OT_UNUSED_VARIABLE(aSignature);
+
+    return OT_ERROR_NOT_CAPABLE;
+}
+
+OT_TOOL_WEAK otError otPlatCryptoEcdsaVerify(const otPlatCryptoEcdsaPublicKey *aPublicKey,
+                                             const otPlatCryptoSha256Hash     *aHash,
+                                             const otPlatCryptoEcdsaSignature *aSignature)
+
+{
+    OT_UNUSED_VARIABLE(aPublicKey);
+    OT_UNUSED_VARIABLE(aHash);
+    OT_UNUSED_VARIABLE(aSignature);
+
+    return OT_ERROR_NOT_CAPABLE;
+}
+#endif // #if OPENTHREAD_CONFIG_ECDSA_ENABLE
+
+#endif // #if !OPENTHREAD_RADIO
+
 #endif // #if OPENTHREAD_CONFIG_CRYPTO_LIB == OPENTHREAD_CONFIG_CRYPTO_LIB_MBEDTLS
 
 //---------------------------------------------------------------------------------------------------------------------
diff --git a/src/core/crypto/ecdsa.hpp b/src/core/crypto/ecdsa.hpp
index e757083..41a693d 100644
--- a/src/core/crypto/ecdsa.hpp
+++ b/src/core/crypto/ecdsa.hpp
@@ -46,6 +46,7 @@
 
 #include "common/error.hpp"
 #include "crypto/sha256.hpp"
+#include "crypto/storage.hpp"
 
 namespace ot {
 namespace Crypto {
@@ -77,6 +78,9 @@
 
     class PublicKey;
     class KeyPair;
+#if OPENTHREAD_CONFIG_PLATFORM_KEY_REFERENCES_ENABLE
+    class KeyPairAsRef;
+#endif
 
     /**
      * This class represents an ECDSA signature.
@@ -90,6 +94,9 @@
     {
         friend class KeyPair;
         friend class PublicKey;
+#if OPENTHREAD_CONFIG_PLATFORM_KEY_REFERENCES_ENABLE
+        friend class KeyPairAsRef;
+#endif
 
     public:
         static constexpr uint8_t kSize = OT_CRYPTO_ECDSA_SIGNATURE_SIZE; ///< Signature size in bytes.
@@ -204,6 +211,105 @@
         }
     };
 
+#if OPENTHREAD_CONFIG_PLATFORM_KEY_REFERENCES_ENABLE
+    /**
+     * This class represents a key pair (public and private keys) as a PSA KeyRef.
+     *
+     */
+    class KeyPairAsRef
+    {
+    public:
+        /**
+         * This constructor initializes a `KeyPairAsRef`.
+         *
+         * @param[in] aKeyRef         PSA key reference to use while using the keypair.
+         */
+        explicit KeyPairAsRef(otCryptoKeyRef aKeyRef = 0) { mKeyRef = aKeyRef; }
+
+        /**
+         * This method generates a new keypair and imports it into PSA ITS.
+         *
+         * @retval kErrorNone         A new key pair was generated successfully.
+         * @retval kErrorNoBufs       Failed to allocate buffer for key generation.
+         * @retval kErrorNotCapable   Feature not supported.
+         * @retval kErrorFailed       Failed to generate key.
+         *
+         */
+        Error Generate(void) const { return otPlatCryptoEcdsaGenerateAndImportKey(mKeyRef); }
+
+        /**
+         * This method imports a new keypair into PSA ITS.
+         *
+         * @param[in] aKeyPair        KeyPair to be imported in DER format.
+         *
+         * @retval kErrorNone         A key pair was imported successfully.
+         * @retval kErrorNotCapable   Feature not supported.
+         * @retval kErrorFailed       Failed to import the key.
+         *
+         */
+        Error ImportKeyPair(const KeyPair &aKeyPair)
+        {
+            return Crypto::Storage::ImportKey(mKeyRef, Storage::kKeyTypeEcdsa, Storage::kKeyAlgorithmEcdsa,
+                                              (Storage::kUsageSignHash | Storage::kUsageVerifyHash),
+                                              Storage::kTypePersistent, aKeyPair.GetDerBytes(),
+                                              aKeyPair.GetDerLength());
+        }
+
+        /**
+         * This method gets the associated public key from the keypair referenced by mKeyRef.
+         *
+         * @param[out] aPublicKey     A reference to a `PublicKey` to output the value.
+         *
+         * @retval kErrorNone      Public key was retrieved successfully, and @p aPublicKey is updated.
+         * @retval kErrorFailed    There was a error exporting the public key from PSA.
+         *
+         */
+        Error GetPublicKey(PublicKey &aPublicKey) const
+        {
+            return otPlatCryptoEcdsaExportPublicKey(mKeyRef, &aPublicKey);
+        }
+
+        /**
+         * This method calculates the ECDSA signature for a hashed message using the private key from keypair
+         * referenced by mKeyRef.
+         *
+         * This method uses the deterministic digital signature generation procedure from RFC 6979.
+         *
+         * @param[in]  aHash               The SHA-256 hash value of the message to use for signature calculation.
+         * @param[out] aSignature          A reference to a `Signature` to output the calculated signature value.
+         *
+         * @retval kErrorNone           The signature was calculated successfully and @p aSignature was updated.
+         * @retval kErrorParse          The key-pair DER format could not be parsed (invalid format).
+         * @retval kErrorInvalidArgs    The @p aHash is invalid.
+         * @retval kErrorNoBufs         Failed to allocate buffer for signature calculation.
+         *
+         */
+        Error Sign(const Sha256::Hash &aHash, Signature &aSignature) const
+        {
+            return otPlatCryptoEcdsaSignUsingKeyRef(mKeyRef, &aHash, &aSignature);
+        }
+
+        /**
+         * This method gets the Key reference for the keypair stored in the PSA.
+         *
+         * @returns The PSA key ref.
+         *
+         */
+        otCryptoKeyRef GetKeyRef(void) const { return mKeyRef; }
+
+        /**
+         * This method sets the Key reference.
+         *
+         * @param[in] aKeyRef         PSA key reference to use while using the keypair.
+         *
+         */
+        void SetKeyRef(otCryptoKeyRef aKeyRef) { mKeyRef = aKeyRef; }
+
+    private:
+        otCryptoKeyRef mKeyRef;
+    };
+#endif
+
     /**
      * This class represents a public key.
      *
@@ -214,6 +320,9 @@
     class PublicKey : public otPlatCryptoEcdsaPublicKey, public Equatable<PublicKey>
     {
         friend class KeyPair;
+#if OPENTHREAD_CONFIG_PLATFORM_KEY_REFERENCES_ENABLE
+        friend class KeyPairAsRef;
+#endif
 
     public:
         static constexpr uint8_t kSize = OT_CRYPTO_ECDSA_PUBLIC_KEY_SIZE; ///< Size of the public key in bytes.
@@ -242,6 +351,7 @@
         {
             return otPlatCryptoEcdsaVerify(this, &aHash, &aSignature);
         }
+
     } OT_TOOL_PACKED_END;
 };
 
diff --git a/src/core/crypto/storage.cpp b/src/core/crypto/storage.cpp
index b9301fc..726daa9 100644
--- a/src/core/crypto/storage.cpp
+++ b/src/core/crypto/storage.cpp
@@ -41,7 +41,7 @@
 #if OPENTHREAD_CONFIG_PLATFORM_KEY_REFERENCES_ENABLE
 Error Key::ExtractKey(uint8_t *aKeyBuffer, uint16_t &aKeyLength) const
 {
-    Error  error;
+    Error  error = kErrorNone;
     size_t readKeyLength;
 
     OT_ASSERT(IsKeyRef());
@@ -55,6 +55,17 @@
 exit:
     return error;
 }
+
+void Storage::DestroyPersistentKeys(void)
+{
+    DestroyKey(kNetworkKeyRef);
+    DestroyKey(kPskcRef);
+    DestroyKey(kActiveDatasetNetworkKeyRef);
+    DestroyKey(kActiveDatasetPskcRef);
+    DestroyKey(kPendingDatasetNetworkKeyRef);
+    DestroyKey(kPendingDatasetPskcRef);
+    DestroyKey(kEcdsaRef);
+}
 #endif
 
 LiteralKey::LiteralKey(const Key &aKey)
diff --git a/src/core/crypto/storage.hpp b/src/core/crypto/storage.hpp
index d435e34..8193943 100644
--- a/src/core/crypto/storage.hpp
+++ b/src/core/crypto/storage.hpp
@@ -57,9 +57,10 @@
  */
 enum KeyType : uint8_t
 {
-    kKeyTypeRaw  = OT_CRYPTO_KEY_TYPE_RAW,  ///< Key Type: Raw Data.
-    kKeyTypeAes  = OT_CRYPTO_KEY_TYPE_AES,  ///< Key Type: AES.
-    kKeyTypeHmac = OT_CRYPTO_KEY_TYPE_HMAC, ///< Key Type: HMAC.
+    kKeyTypeRaw   = OT_CRYPTO_KEY_TYPE_RAW,   ///< Key Type: Raw Data.
+    kKeyTypeAes   = OT_CRYPTO_KEY_TYPE_AES,   ///< Key Type: AES.
+    kKeyTypeHmac  = OT_CRYPTO_KEY_TYPE_HMAC,  ///< Key Type: HMAC.
+    kKeyTypeEcdsa = OT_CRYPTO_KEY_TYPE_ECDSA, ///< Key Type: ECDSA.
 };
 
 /**
@@ -71,13 +72,15 @@
     kKeyAlgorithmVendor     = OT_CRYPTO_KEY_ALG_VENDOR,       ///< Key Algorithm: Vendor Defined.
     kKeyAlgorithmAesEcb     = OT_CRYPTO_KEY_ALG_AES_ECB,      ///< Key Algorithm: AES ECB.
     kKeyAlgorithmHmacSha256 = OT_CRYPTO_KEY_ALG_HMAC_SHA_256, ///< Key Algorithm: HMAC SHA-256.
+    kKeyAlgorithmEcdsa      = OT_CRYPTO_KEY_ALG_ECDSA,        ///< Key Algorithm: ECDSA.
 };
 
-constexpr uint8_t kUsageNone     = OT_CRYPTO_KEY_USAGE_NONE;      ///< Key Usage: Key Usage is empty.
-constexpr uint8_t kUsageExport   = OT_CRYPTO_KEY_USAGE_EXPORT;    ///< Key Usage: Key can be exported.
-constexpr uint8_t kUsageEncrypt  = OT_CRYPTO_KEY_USAGE_ENCRYPT;   ///< Key Usage: Encrypt (vendor defined).
-constexpr uint8_t kUsageDecrypt  = OT_CRYPTO_KEY_USAGE_DECRYPT;   ///< Key Usage: AES ECB.
-constexpr uint8_t kUsageSignHash = OT_CRYPTO_KEY_USAGE_SIGN_HASH; ///< Key Usage: HMAC SHA-256.
+constexpr uint8_t kUsageNone       = OT_CRYPTO_KEY_USAGE_NONE;        ///< Key Usage: Key Usage is empty.
+constexpr uint8_t kUsageExport     = OT_CRYPTO_KEY_USAGE_EXPORT;      ///< Key Usage: Key can be exported.
+constexpr uint8_t kUsageEncrypt    = OT_CRYPTO_KEY_USAGE_ENCRYPT;     ///< Key Usage: Encrypt (vendor defined).
+constexpr uint8_t kUsageDecrypt    = OT_CRYPTO_KEY_USAGE_DECRYPT;     ///< Key Usage: AES ECB.
+constexpr uint8_t kUsageSignHash   = OT_CRYPTO_KEY_USAGE_SIGN_HASH;   ///< Key Usage: Sign Hash.
+constexpr uint8_t kUsageVerifyHash = OT_CRYPTO_KEY_USAGE_VERIFY_HASH; ///< Key Usage: Verify Hash.
 
 /**
  * This enumeration defines the key storage types.
@@ -102,6 +105,7 @@
 constexpr KeyRef kActiveDatasetPskcRef        = OPENTHREAD_CONFIG_PSA_ITS_NVM_OFFSET + 4;
 constexpr KeyRef kPendingDatasetNetworkKeyRef = OPENTHREAD_CONFIG_PSA_ITS_NVM_OFFSET + 5;
 constexpr KeyRef kPendingDatasetPskcRef       = OPENTHREAD_CONFIG_PSA_ITS_NVM_OFFSET + 6;
+constexpr KeyRef kEcdsaRef                    = OPENTHREAD_CONFIG_PSA_ITS_NVM_OFFSET + 7;
 
 /**
  * Determine if a given `KeyRef` is valid or not.
@@ -186,6 +190,12 @@
  */
 inline bool HasKey(KeyRef aKeyRef) { return otPlatCryptoHasKey(aKeyRef); }
 
+/**
+ * Delete all the persistent keys stored in PSA ITS.
+ *
+ */
+void DestroyPersistentKeys(void);
+
 } // namespace Storage
 
 #endif // OPENTHREAD_CONFIG_PLATFORM_KEY_REFERENCES_ENABLE
@@ -204,7 +214,7 @@
      * This method sets the `Key` as a literal key from a given byte array and length.
      *
      * @param[in] aKeyBytes   A pointer to buffer containing the key.
-     * @param[in] aKeyLength  The key length (number of bytes in @p akeyBytes).
+     * @param[in] aKeyLength  The key length (number of bytes in @p aKeyBytes).
      *
      */
     void Set(const uint8_t *aKeyBytes, uint16_t aKeyLength)
diff --git a/src/core/diags/README.md b/src/core/diags/README.md
index b137b14..2ddb658 100644
--- a/src/core/diags/README.md
+++ b/src/core/diags/README.md
@@ -10,6 +10,7 @@
 - [diag start](#diag-start)
 - [diag channel](#diag-channel)
 - [diag cw](#diag-cw-start)
+- [diag stream](#diag-stream-start)
 - [diag power](#diag-power)
 - [diag powersettings](#diag-powersettings)
 - [diag send](#diag-send-packets-length)
@@ -76,6 +77,24 @@
 Done
 ```
 
+### diag stream start
+
+Start transmitting a stream of characters.
+
+```bash
+> diag stream start
+Done
+```
+
+### diag stream stop
+
+Stop transmitting a stream of characters.
+
+```bash
+> diag stream stop
+Done
+```
+
 ### diag power
 
 Get the tx power value(dBm) for diagnostics module.
diff --git a/src/core/diags/factory_diags.cpp b/src/core/diags/factory_diags.cpp
index 542b493..5eed151 100644
--- a/src/core/diags/factory_diags.cpp
+++ b/src/core/diags/factory_diags.cpp
@@ -79,6 +79,7 @@
     {"rawpowersetting", &Diags::ProcessRawPowerSetting},
     {"start", &Diags::ProcessStart},
     {"stop", &Diags::ProcessStop},
+    {"stream", &Diags::ProcessStream},
 };
 
 Diags::Diags(Instance &aInstance)
@@ -196,6 +197,7 @@
     {"start", &Diags::ProcessStart},
     {"stats", &Diags::ProcessStats},
     {"stop", &Diags::ProcessStop},
+    {"stream", &Diags::ProcessStream},
 };
 
 Diags::Diags(Instance &aInstance)
@@ -573,6 +575,27 @@
     return error;
 }
 
+Error Diags::ProcessStream(uint8_t aArgsLength, char *aArgs[], char *aOutput, size_t aOutputMaxLen)
+{
+    Error error = kErrorInvalidArgs;
+
+    VerifyOrExit(otPlatDiagModeGet(), error = kErrorInvalidState);
+    VerifyOrExit(aArgsLength > 0, error = kErrorInvalidArgs);
+
+    if (strcmp(aArgs[0], "start") == 0)
+    {
+        error = otPlatDiagRadioTransmitStream(&GetInstance(), true);
+    }
+    else if (strcmp(aArgs[0], "stop") == 0)
+    {
+        error = otPlatDiagRadioTransmitStream(&GetInstance(), false);
+    }
+
+exit:
+    AppendErrorResult(error, aOutput, aOutputMaxLen);
+    return error;
+}
+
 Error Diags::GetPowerSettings(uint8_t aChannel, PowerSettings &aPowerSettings)
 {
     aPowerSettings.mRawPowerSetting.mLength = RawPowerSetting::kMaxDataSize;
@@ -939,6 +962,14 @@
     return OT_ERROR_NOT_IMPLEMENTED;
 }
 
+OT_TOOL_WEAK otError otPlatDiagRadioTransmitStream(otInstance *aInstance, bool aEnable)
+{
+    OT_UNUSED_VARIABLE(aInstance);
+    OT_UNUSED_VARIABLE(aEnable);
+
+    return OT_ERROR_NOT_IMPLEMENTED;
+}
+
 OT_TOOL_WEAK otError otPlatDiagRadioGetPowerSettings(otInstance *aInstance,
                                                      uint8_t     aChannel,
                                                      int16_t    *aTargetPower,
diff --git a/src/core/diags/factory_diags.hpp b/src/core/diags/factory_diags.hpp
index 9115e51..d7ac648 100644
--- a/src/core/diags/factory_diags.hpp
+++ b/src/core/diags/factory_diags.hpp
@@ -193,6 +193,7 @@
     Error ProcessStart(uint8_t aArgsLength, char *aArgs[], char *aOutput, size_t aOutputMaxLen);
     Error ProcessStats(uint8_t aArgsLength, char *aArgs[], char *aOutput, size_t aOutputMaxLen);
     Error ProcessStop(uint8_t aArgsLength, char *aArgs[], char *aOutput, size_t aOutputMaxLen);
+    Error ProcessStream(uint8_t aArgsLength, char *aArgs[], char *aOutput, size_t aOutputMaxLen);
 #if OPENTHREAD_RADIO && !OPENTHREAD_RADIO_CLI
     Error ProcessEcho(uint8_t aArgsLength, char *aArgs[], char *aOutput, size_t aOutputMaxLen);
 #endif
diff --git a/src/core/ftd.cmake b/src/core/ftd.cmake
index 928a2b4..dd6dae2 100644
--- a/src/core/ftd.cmake
+++ b/src/core/ftd.cmake
@@ -47,6 +47,4 @@
         ot-config
 )
 
-if(NOT OT_EXCLUDE_TCPLP_LIB)
-    target_link_libraries(openthread-ftd PRIVATE tcplp-ftd)
-endif()
+target_link_libraries(openthread-ftd PRIVATE tcplp-ftd)
diff --git a/src/core/mac/data_poll_sender.cpp b/src/core/mac/data_poll_sender.cpp
index 84486ff..acc968c 100644
--- a/src/core/mac/data_poll_sender.cpp
+++ b/src/core/mac/data_poll_sender.cpp
@@ -542,8 +542,10 @@
 
 uint32_t DataPollSender::GetDefaultPollPeriod(void) const
 {
-    uint32_t period    = Time::SecToMsec(Get<Mle::MleRouter>().GetTimeout());
     uint32_t pollAhead = static_cast<uint32_t>(kRetxPollPeriod) * kMaxPollRetxAttempts;
+    uint32_t period;
+
+    period = Time::SecToMsec(Min(Get<Mle::MleRouter>().GetTimeout(), Time::MsecToSec(TimerMilli::kMaxDelay)));
 
 #if OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_ENABLE && OPENTHREAD_CONFIG_MAC_CSL_AUTO_SYNC_ENABLE
     if (Get<Mac::Mac>().IsCslEnabled())
diff --git a/src/core/mac/data_poll_sender.hpp b/src/core/mac/data_poll_sender.hpp
index 13e11be..2327bd9 100644
--- a/src/core/mac/data_poll_sender.hpp
+++ b/src/core/mac/data_poll_sender.hpp
@@ -295,7 +295,7 @@
     bool    mEnabled : 1;              // Indicates whether data polling is enabled/started.
     bool    mAttachMode : 1;           // Indicates whether in attach mode (to use attach poll period).
     bool    mRetxMode : 1;             // Indicates whether last poll tx failed at mac/radio layer (poll retx mode).
-    uint8_t mPollTimeoutCounter : 4;   // Poll timeouts counter (0 to `kQuickPollsAfterTimout`).
+    uint8_t mPollTimeoutCounter : 4;   // Poll timeouts counter (0 to `kQuickPollsAfterTimeout`).
     uint8_t mPollTxFailureCounter : 4; // Poll tx failure counter (0 to `kMaxPollRetxAttempts`).
     uint8_t mRemainingFastPolls : 4;   // Number of remaining fast polls when in transient fast polling mode.
 };
diff --git a/src/core/mac/mac.cpp b/src/core/mac/mac.cpp
index 7a12913..a29ee3f 100644
--- a/src/core/mac/mac.cpp
+++ b/src/core/mac/mac.cpp
@@ -1201,6 +1201,8 @@
 #if OPENTHREAD_CONFIG_MLE_LINK_METRICS_SUBJECT_ENABLE
         neighbor->AggregateLinkMetrics(/* aSeriesId */ 0, aAckFrame->GetType(), aAckFrame->GetLqi(),
                                        aAckFrame->GetRssi());
+#endif
+#if OPENTHREAD_CONFIG_MLE_LINK_METRICS_INITIATOR_ENABLE
         ProcessEnhAckProbing(*aAckFrame, *neighbor);
 #endif
 #if OPENTHREAD_FTD
@@ -1305,11 +1307,11 @@
     if (!aFrame.IsEmpty())
     {
         RadioType  radio          = aFrame.GetRadioType();
-        RadioTypes requriedRadios = mLinks.GetTxFrames().GetRequiredRadioTypes();
+        RadioTypes requiredRadios = mLinks.GetTxFrames().GetRequiredRadioTypes();
 
         Get<RadioSelector>().UpdateOnSendDone(aFrame, aError);
 
-        if (requriedRadios.IsEmpty())
+        if (requiredRadios.IsEmpty())
         {
             // If the "required radio type set" is empty, successful
             // tx over any radio link is sufficient for overall tx to
@@ -1330,7 +1332,7 @@
             // `mTxError` starts as `kErrorNone` and we update it
             // if tx over any link in the set fails.
 
-            if (requriedRadios.Contains(radio) && (aError != kErrorNone))
+            if (requiredRadios.Contains(radio) && (aError != kErrorNone))
             {
                 LogDebg("Frame tx failed on required radio link %s with error %s", RadioTypeToString(radio),
                         ErrorToString(aError));
@@ -2304,7 +2306,7 @@
 }
 #endif // OPENTHREAD_FTD && OPENTHREAD_CONFIG_MAC_CSL_TRANSMITTER_ENABLE
 
-#if OPENTHREAD_CONFIG_MLE_LINK_METRICS_SUBJECT_ENABLE
+#if OPENTHREAD_CONFIG_MLE_LINK_METRICS_INITIATOR_ENABLE
 void Mac::ProcessEnhAckProbing(const RxFrame &aFrame, const Neighbor &aNeighbor)
 {
     constexpr uint8_t kEnhAckProbingIeMaxLen = 2;
@@ -2320,11 +2322,11 @@
     dataLen = enhAckProbingIe->GetLength() - sizeof(VendorIeHeader);
     VerifyOrExit(dataLen <= kEnhAckProbingIeMaxLen);
 
-    Get<LinkMetrics::LinkMetrics>().ProcessEnhAckIeData(data, dataLen, aNeighbor);
+    Get<LinkMetrics::Initiator>().ProcessEnhAckIeData(data, dataLen, aNeighbor);
 exit:
     return;
 }
-#endif // OPENTHREAD_CONFIG_MLE_LINK_METRICS_SUBJECT_ENABLE
+#endif // OPENTHREAD_CONFIG_MLE_LINK_METRICS_INITIATOR_ENABLE
 
 #if OPENTHREAD_CONFIG_MAC_FILTER_ENABLE && OPENTHREAD_CONFIG_RADIO_LINK_IEEE_802_15_4_ENABLE
 void Mac::SetRadioFilterEnabled(bool aFilterEnabled)
diff --git a/src/core/mac/mac.hpp b/src/core/mac/mac.hpp
index dc0fd7b..3566109 100644
--- a/src/core/mac/mac.hpp
+++ b/src/core/mac/mac.hpp
@@ -587,6 +587,12 @@
      */
     bool IsEnabled(void) const { return mEnabled; }
 
+    /**
+     * This method clears the Mode2Key stored in PSA ITS.
+     *
+     */
+    void ClearMode2Key(void) { mMode2KeyMaterial.Clear(); }
+
 #if OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_ENABLE
     /**
      * This method gets the CSL channel.
@@ -789,7 +795,7 @@
 #if OPENTHREAD_FTD && OPENTHREAD_CONFIG_MAC_CSL_TRANSMITTER_ENABLE
     void ProcessCsl(const RxFrame &aFrame, const Address &aSrcAddr);
 #endif
-#if OPENTHREAD_CONFIG_MLE_LINK_METRICS_SUBJECT_ENABLE
+#if OPENTHREAD_CONFIG_MLE_LINK_METRICS_INITIATOR_ENABLE
     void ProcessEnhAckProbing(const RxFrame &aFrame, const Neighbor &aNeighbor);
 #endif
     static const char *OperationToString(Operation aOperation);
diff --git a/src/core/mac/mac_filter.cpp b/src/core/mac/mac_filter.cpp
index a5df54a..97604a3 100644
--- a/src/core/mac/mac_filter.cpp
+++ b/src/core/mac/mac_filter.cpp
@@ -266,7 +266,7 @@
     {
         // Clear the previous RSS average to ensure the fixed RSS
         // value takes effect quickly.
-        aNeighbor->GetLinkInfo().CleaAverageRss();
+        aNeighbor->GetLinkInfo().ClearAverageRss();
     }
 
 exit:
diff --git a/src/core/mac/mac_frame.hpp b/src/core/mac/mac_frame.hpp
index 13b6033..78924bf 100644
--- a/src/core/mac/mac_frame.hpp
+++ b/src/core/mac/mac_frame.hpp
@@ -381,7 +381,7 @@
      * @param[in] aVerion        Frame version.
      * @param[in] aAddrs         Frame source and destination addresses (each can be none, short, or extended).
      * @param[in] aPanIds        Source and destination PAN IDs.
-     * @param[in] aSeucirtyLevel Frame security level.
+     * @param[in] aSecurityLevel Frame security level.
      * @param[in] aKeyIdMode     Frame security key ID mode.
      *
      */
diff --git a/src/core/mac/mac_types.cpp b/src/core/mac/mac_types.cpp
index 83fab4b..7ed4b1b 100644
--- a/src/core/mac/mac_types.cpp
+++ b/src/core/mac/mac_types.cpp
@@ -299,7 +299,7 @@
 #endif
 }
 
-void KeyMaterial::ExtractKey(Key &aKey)
+void KeyMaterial::ExtractKey(Key &aKey) const
 {
 #if OPENTHREAD_CONFIG_PLATFORM_KEY_REFERENCES_ENABLE
     aKey.Clear();
diff --git a/src/core/mac/mac_types.hpp b/src/core/mac/mac_types.hpp
index 9ee7398..6a73244 100644
--- a/src/core/mac/mac_types.hpp
+++ b/src/core/mac/mac_types.hpp
@@ -540,7 +540,7 @@
      * @param[out] aKey  A reference to the output the key.
      *
      */
-    void ExtractKey(Key &aKey);
+    void ExtractKey(Key &aKey) const;
 
     /**
      * This method converts `KeyMaterial` to a `Crypto::Key`.
diff --git a/src/core/mac/sub_mac.cpp b/src/core/mac/sub_mac.cpp
index 15d8555..755931c 100644
--- a/src/core/mac/sub_mac.cpp
+++ b/src/core/mac/sub_mac.cpp
@@ -285,7 +285,7 @@
 
     if (!ShouldHandleTransmitSecurity() && aFrame != nullptr && aFrame->mInfo.mRxInfo.mAckedWithSecEnhAck)
     {
-        SignalFrameCounterUsed(aFrame->mInfo.mRxInfo.mAckFrameCounter);
+        SignalFrameCounterUsed(aFrame->mInfo.mRxInfo.mAckFrameCounter, aFrame->mInfo.mRxInfo.mAckKeyId);
     }
 
 #if OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_ENABLE
@@ -390,7 +390,7 @@
         uint32_t frameCounter = GetFrameCounter();
 
         mTransmitFrame.SetFrameCounter(frameCounter);
-        SignalFrameCounterUsed(frameCounter);
+        SignalFrameCounterUsed(frameCounter, mKeyId);
     }
 
     extAddress = &GetExtAddress();
@@ -642,6 +642,7 @@
 void SubMac::SignalFrameCounterUsedOnTxDone(const TxFrame &aFrame)
 {
     uint8_t  keyIdMode;
+    uint8_t  keyId;
     uint32_t frameCounter;
     bool     allowError = false;
 
@@ -666,7 +667,9 @@
     VerifyOrExit(keyIdMode == Frame::kKeyIdMode1);
 
     VerifyOrExit(aFrame.GetFrameCounter(frameCounter) == kErrorNone, OT_ASSERT(allowError));
-    SignalFrameCounterUsed(frameCounter);
+    VerifyOrExit(aFrame.GetKeyId(keyId) == kErrorNone, OT_ASSERT(allowError));
+
+    SignalFrameCounterUsed(frameCounter, keyId);
 
 exit:
     return;
@@ -961,8 +964,10 @@
     return;
 }
 
-void SubMac::SignalFrameCounterUsed(uint32_t aFrameCounter)
+void SubMac::SignalFrameCounterUsed(uint32_t aFrameCounter, uint8_t aKeyId)
 {
+    VerifyOrExit(aKeyId == mKeyId);
+
     mCallbacks.FrameCounterUsed(aFrameCounter);
 
     // It not always guaranteed that this method is invoked in order
@@ -1116,7 +1121,7 @@
      *            sample                   sleep                        sample                    sleep
      *
      * When the radio doesn't support receive-timing:
-     *   The handler will be called twice per CSL period: at the begining of sample and sleep. When the handler is
+     *   The handler will be called twice per CSL period: at the beginning of sample and sleep. When the handler is
      *   called, it will explicitly change the radio state due to the current state by calling `Radio::Receive` or
      *   `Radio::Sleep`.
      *
@@ -1160,7 +1165,7 @@
 
         Get<Radio>().UpdateCslSampleTime(mCslSampleTime.GetValue());
 
-        // Scedule reception window for any state except RX - so that CSL RX Window has lower priority
+        // Schedule reception window for any state except RX - so that CSL RX Window has lower priority
         // than scanning or RX after the data poll.
         if (RadioSupportsReceiveTiming() && (mState != kStateDisabled) && (mState != kStateReceive))
         {
@@ -1190,8 +1195,8 @@
                               (Get<Radio>().GetCslAccuracy() + mCslParentAccuracy.GetClockAccuracy()) / 1000000);
     semiWindow += mCslParentAccuracy.GetUncertaintyInMicrosec() + Get<Radio>().GetCslUncertainty() * 10;
 
-    aAhead = Min(semiPeriod, semiWindow + kCslReceiveTimeAhead);
-    aAfter = Min(semiPeriod, semiWindow + kMinCslWindow);
+    aAhead = Min(semiPeriod, semiWindow + kMinReceiveOnAhead + kCslReceiveTimeAhead);
+    aAfter = Min(semiPeriod, semiWindow + kMinReceiveOnAfter);
 }
 #endif // OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_ENABLE
 
diff --git a/src/core/mac/sub_mac.hpp b/src/core/mac/sub_mac.hpp
index a70ffb3..a4c55de 100644
--- a/src/core/mac/sub_mac.hpp
+++ b/src/core/mac/sub_mac.hpp
@@ -478,6 +478,17 @@
     const KeyMaterial &GetNextMacKey(void) const { return mNextKey; }
 
     /**
+     * This method clears the stored MAC keys.
+     *
+     */
+    void ClearMacKeys(void)
+    {
+        mPrevKey.Clear();
+        mCurrKey.Clear();
+        mNextKey.Clear();
+    }
+
+    /**
      * This method returns the current MAC frame counter value.
      *
      * @returns The current MAC frame counter value.
@@ -562,9 +573,9 @@
     };
 
 #if OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_ENABLE
-    // CSL receive window for the longest possible frame and
-    // ack duration.
-    static constexpr uint32_t kMinCslWindow = OPENTHREAD_CONFIG_CSL_MIN_RECEIVE_ON;
+    // Radio on times needed before and after MHR time for proper frame detection
+    static constexpr uint32_t kMinReceiveOnAhead = OPENTHREAD_CONFIG_MIN_RECEIVE_ON_AHEAD;
+    static constexpr uint32_t kMinReceiveOnAfter = OPENTHREAD_CONFIG_MIN_RECEIVE_ON_AFTER;
 
     // CSL receivers would wake up `kCslReceiveTimeAhead` earlier
     // than expected sample window. The value is in usec.
@@ -604,7 +615,7 @@
     bool ShouldHandleTransmitTargetTime(void) const;
 
     void ProcessTransmitSecurity(void);
-    void SignalFrameCounterUsed(uint32_t aFrameCounter);
+    void SignalFrameCounterUsed(uint32_t aFrameCounter, uint8_t aKeyId);
     void StartCsmaBackoff(void);
     void StartTimerForBackoff(uint8_t aBackoffExponent);
     void BeginTransmit(void);
diff --git a/src/core/meshcop/announce_begin_client.cpp b/src/core/meshcop/announce_begin_client.cpp
index 47e7fe7..f0cde45 100644
--- a/src/core/meshcop/announce_begin_client.cpp
+++ b/src/core/meshcop/announce_begin_client.cpp
@@ -85,7 +85,7 @@
 
     SuccessOrExit(error = Get<Tmf::Agent>().SendMessage(*message, messageInfo));
 
-    LogInfo("sent announce begin query");
+    LogInfo("Sent %s", UriToString<kUriAnnounceBegin>());
 
 exit:
     FreeMessageOnError(message, error);
diff --git a/src/core/meshcop/border_agent.cpp b/src/core/meshcop/border_agent.cpp
index dc06648..d15e77c 100644
--- a/src/core/meshcop/border_agent.cpp
+++ b/src/core/meshcop/border_agent.cpp
@@ -41,6 +41,7 @@
 #include "common/instance.hpp"
 #include "common/locator_getters.hpp"
 #include "common/log.hpp"
+#include "common/settings.hpp"
 #include "meshcop/meshcop.hpp"
 #include "meshcop/meshcop_tlvs.hpp"
 #include "thread/thread_netif.hpp"
@@ -225,10 +226,53 @@
     , mTimer(aInstance)
     , mState(kStateStopped)
     , mUdpProxyPort(0)
+#if OPENTHREAD_CONFIG_BORDER_AGENT_ID_ENABLE
+    , mIdInitialized(false)
+#endif
 {
     mCommissionerAloc.InitAsThreadOriginRealmLocalScope();
 }
 
+#if OPENTHREAD_CONFIG_BORDER_AGENT_ID_ENABLE
+Error BorderAgent::GetId(Id &aId)
+{
+    Error                   error = kErrorNone;
+    Settings::BorderAgentId id;
+
+    VerifyOrExit(!mIdInitialized, error = kErrorNone);
+
+    if (Get<Settings>().Read(id) != kErrorNone)
+    {
+        Random::NonCrypto::FillBuffer(id.GetId().mId, sizeof(id));
+        SuccessOrExit(error = Get<Settings>().Save(id));
+    }
+
+    mId            = id.GetId();
+    mIdInitialized = true;
+
+exit:
+    if (error == kErrorNone)
+    {
+        aId = mId;
+    }
+    return error;
+}
+
+Error BorderAgent::SetId(const Id &aId)
+{
+    Error                   error = kErrorNone;
+    Settings::BorderAgentId id;
+
+    id.SetId(aId);
+    SuccessOrExit(error = Get<Settings>().Save(id));
+    mId            = aId;
+    mIdInitialized = true;
+
+exit:
+    return error;
+}
+#endif // OPENTHREAD_CONFIG_BORDER_AGENT_ID_ENABLE
+
 void BorderAgent::HandleNotifierEvents(Events aEvents)
 {
     VerifyOrExit(aEvents.ContainsAny(kEventThreadRoleChanged | kEventCommissionerStateChanged));
@@ -271,7 +315,7 @@
 
     VerifyOrExit(udpEncapHeader.GetSourcePort() > 0 && udpEncapHeader.GetDestinationPort() > 0, error = kErrorDrop);
 
-    VerifyOrExit((message = Get<Ip6::Udp>().NewMessage(0)) != nullptr, error = kErrorNoBufs);
+    VerifyOrExit((message = Get<Ip6::Udp>().NewMessage()) != nullptr, error = kErrorNoBufs);
     SuccessOrExit(error = message->AppendBytesFromMessage(aMessage, offset, length));
 
     messageInfo.SetSockPort(udpEncapHeader.GetSourcePort());
diff --git a/src/core/meshcop/border_agent.hpp b/src/core/meshcop/border_agent.hpp
index 2b33d54..58f5b51 100644
--- a/src/core/meshcop/border_agent.hpp
+++ b/src/core/meshcop/border_agent.hpp
@@ -59,6 +59,8 @@
     friend class Tmf::SecureAgent;
 
 public:
+    typedef otBorderAgentId Id; ///< Border Agent ID.
+
     /**
      * This enumeration defines the Border Agent state.
      *
@@ -78,6 +80,38 @@
      */
     explicit BorderAgent(Instance &aInstance);
 
+#if OPENTHREAD_CONFIG_BORDER_AGENT_ID_ENABLE
+    /**
+     * Gets the randomly generated Border Agent ID.
+     *
+     * The ID is saved in persistent storage and survives reboots. The typical use case of the ID is to
+     * be published in the MeshCoP mDNS service as the `id` TXT value for the client to identify this
+     * Border Router/Agent device.
+     *
+     * @param[out] aId  Reference to return the Border Agent ID.
+     *
+     * @retval kErrorNone  If successfully retrieved the Border Agent ID.
+     * @retval ...         If failed to retrieve the Border Agent ID.
+     *
+     */
+    Error GetId(Id &aId);
+
+    /**
+     * Sets the Border Agent ID.
+     *
+     * The Border Agent ID will be saved in persistent storage and survive reboots. It's required
+     * to set the ID only once after factory reset. If the ID has never been set by calling this
+     * method, a random ID will be generated and returned when `GetId()` is called.
+     *
+     * @param[out] aId  specifies the Border Agent ID.
+     *
+     * @retval kErrorNone  If successfully set the Border Agent ID.
+     * @retval ...         If failed to set the Border Agent ID.
+     *
+     */
+    Error SetId(const Id &aId);
+#endif
+
     /**
      * This method gets the UDP port of this service.
      *
@@ -178,6 +212,10 @@
     TimeoutTimer mTimer;
     State        mState;
     uint16_t     mUdpProxyPort;
+#if OPENTHREAD_CONFIG_BORDER_AGENT_ID_ENABLE
+    Id   mId;
+    bool mIdInitialized;
+#endif
 };
 
 DeclareTmfHandler(BorderAgent, kUriRelayRx);
@@ -195,6 +233,7 @@
 } // namespace MeshCoP
 
 DefineMapEnum(otBorderAgentState, MeshCoP::BorderAgent::State);
+DefineCoreType(otBorderAgentId, MeshCoP::BorderAgent::Id);
 
 } // namespace ot
 
diff --git a/src/core/meshcop/commissioner.cpp b/src/core/meshcop/commissioner.cpp
index 5bd024b..0bac432 100644
--- a/src/core/meshcop/commissioner.cpp
+++ b/src/core/meshcop/commissioner.cpp
@@ -695,7 +695,7 @@
     SuccessOrExit(error = Get<Tmf::Agent>().SendMessage(*message, messageInfo,
                                                         Commissioner::HandleMgmtCommissionerGetResponse, this));
 
-    LogInfo("sent MGMT_COMMISSIONER_GET.req to leader");
+    LogInfo("Sent %s to leader", UriToString<kUriCommissionerGet>());
 
 exit:
     FreeMessageOnError(message, error);
@@ -718,7 +718,7 @@
     OT_UNUSED_VARIABLE(aMessageInfo);
 
     VerifyOrExit(aResult == kErrorNone && aMessage->GetCode() == Coap::kCodeChanged);
-    LogInfo("received MGMT_COMMISSIONER_GET response");
+    LogInfo("Received %s response", UriToString<kUriCommissionerGet>());
 
 exit:
     return;
@@ -764,7 +764,7 @@
     SuccessOrExit(error = Get<Tmf::Agent>().SendMessage(*message, messageInfo,
                                                         Commissioner::HandleMgmtCommissionerSetResponse, this));
 
-    LogInfo("sent MGMT_COMMISSIONER_SET.req to leader");
+    LogInfo("Sent %s to leader", UriToString<kUriCommissionerSet>());
 
 exit:
     FreeMessageOnError(message, error);
@@ -787,7 +787,7 @@
     OT_UNUSED_VARIABLE(aMessageInfo);
 
     VerifyOrExit(aResult == kErrorNone && aMessage->GetCode() == Coap::kCodeChanged);
-    LogInfo("received MGMT_COMMISSIONER_SET response");
+    LogInfo("Received %s response", UriToString<kUriCommissionerSet>());
 
 exit:
     return;
@@ -813,7 +813,7 @@
     SuccessOrExit(
         error = Get<Tmf::Agent>().SendMessage(*message, messageInfo, Commissioner::HandleLeaderPetitionResponse, this));
 
-    LogInfo("sent petition");
+    LogInfo("Sent %s", UriToString<kUriLeaderPetition>());
 
 exit:
     FreeMessageOnError(message, error);
@@ -842,7 +842,7 @@
     VerifyOrExit(aResult == kErrorNone && aMessage->GetCode() == Coap::kCodeChanged,
                  retransmit = (mState == kStatePetition));
 
-    LogInfo("received Leader Petition response");
+    LogInfo("Received %s response", UriToString<kUriLeaderPetition>());
 
     SuccessOrExit(Tlv::Find<StateTlv>(*aMessage, state));
     VerifyOrExit(state == StateTlv::kAccept, IgnoreError(Stop(kDoNotSendKeepAlive)));
@@ -900,7 +900,7 @@
     SuccessOrExit(error = Get<Tmf::Agent>().SendMessage(*message, messageInfo,
                                                         Commissioner::HandleLeaderKeepAliveResponse, this));
 
-    LogInfo("sent keep alive");
+    LogInfo("Sent %s", UriToString<kUriLeaderKeepAlive>());
 
 exit:
     FreeMessageOnError(message, error);
@@ -928,7 +928,7 @@
     VerifyOrExit(aResult == kErrorNone && aMessage->GetCode() == Coap::kCodeChanged,
                  IgnoreError(Stop(kDoNotSendKeepAlive)));
 
-    LogInfo("received Leader keep-alive response");
+    LogInfo("Received %s response", UriToString<kUriLeaderKeepAlive>());
 
     SuccessOrExit(Tlv::Find<StateTlv>(*aMessage, state));
     VerifyOrExit(state == StateTlv::kAccept, IgnoreError(Stop(kDoNotSendKeepAlive)));
@@ -985,7 +985,7 @@
     {
         if (mJoinerIid != joinerIid)
         {
-            LogNote("Ignore Relay Receive (%s, 0x%04x), session in progress with (%s, 0x%04x)",
+            LogNote("Ignore %s (%s, 0x%04x), session in progress with (%s, 0x%04x)", UriToString<kUriRelayRx>(),
                     joinerIid.ToString().AsCString(), joinerRloc, mJoinerIid.ToString().AsCString(), mJoinerRloc);
 
             ExitNow();
@@ -995,7 +995,7 @@
     mJoinerPort = joinerPort;
     mJoinerRloc = joinerRloc;
 
-    LogInfo("Received Relay Receive (%s, 0x%04x)", mJoinerIid.ToString().AsCString(), mJoinerRloc);
+    LogInfo("Received %s (%s, 0x%04x)", UriToString<kUriRelayRx>(), mJoinerIid.ToString().AsCString(), mJoinerRloc);
 
     aMessage.SetOffset(offset);
     SuccessOrExit(error = aMessage.SetLength(offset + length));
@@ -1026,11 +1026,11 @@
     VerifyOrExit(mState == kStateActive);
     VerifyOrExit(aMessage.IsConfirmablePostRequest());
 
-    LogInfo("received dataset changed");
+    LogInfo("Received %s", UriToString<kUriDatasetChanged>());
 
     SuccessOrExit(Get<Tmf::Agent>().SendEmptyAck(aMessage, aMessageInfo));
 
-    LogInfo("sent dataset changed acknowledgment");
+    LogInfo("Sent %s ack", UriToString<kUriDatasetChanged>());
 
 exit:
     return;
@@ -1046,7 +1046,7 @@
 
     VerifyOrExit(mState == kStateActive);
 
-    LogInfo("received joiner finalize");
+    LogInfo("Received %s", UriToString<kUriJoinerFinalize>());
 
     switch (Tlv::Find<ProvisioningUrlTlv>(aMessage, provisioningUrl))
     {
@@ -1116,7 +1116,7 @@
         RemoveJoiner(*mActiveJoiner, kRemoveJoinerDelay);
     }
 
-    LogInfo("sent joiner finalize response");
+    LogInfo("Sent %s response", UriToString<kUriJoinerFinalize>());
 
 exit:
     FreeMessageOnError(message, error);
diff --git a/src/core/meshcop/dataset.cpp b/src/core/meshcop/dataset.cpp
index 9ee0bb4..5d9a028 100644
--- a/src/core/meshcop/dataset.cpp
+++ b/src/core/meshcop/dataset.cpp
@@ -83,7 +83,7 @@
     SuccessOrExit(error = Random::Crypto::FillBuffer(mExtendedPanId.m8, sizeof(mExtendedPanId.m8)));
     SuccessOrExit(error = AsCoreType(&mMeshLocalPrefix).GenerateRandomUla());
 
-    snprintf(mNetworkName.m8, sizeof(mNetworkName), "OpenThread-%04x", mPanId);
+    snprintf(mNetworkName.m8, sizeof(mNetworkName), "%s-%04x", NetworkName::kNetworkNameInit, mPanId);
 
     mComponents.mIsActiveTimestampPresent = true;
     mComponents.mIsNetworkKeyPresent      = true;
diff --git a/src/core/meshcop/dataset_manager.cpp b/src/core/meshcop/dataset_manager.cpp
index 49d72c5..7dcef3d 100644
--- a/src/core/meshcop/dataset_manager.cpp
+++ b/src/core/meshcop/dataset_manager.cpp
@@ -116,7 +116,7 @@
 {
     Error error = kErrorNone;
     int   compare;
-    bool  isNetworkkeyUpdated = false;
+    bool  isNetworkKeyUpdated = false;
 
     if (aDataset.GetTimestamp(GetType(), mTimestamp) == kErrorNone)
     {
@@ -124,13 +124,13 @@
 
         if (IsActiveDataset())
         {
-            SuccessOrExit(error = aDataset.ApplyConfiguration(GetInstance(), &isNetworkkeyUpdated));
+            SuccessOrExit(error = aDataset.ApplyConfiguration(GetInstance(), &isNetworkKeyUpdated));
         }
     }
 
     compare = Timestamp::Compare(mTimestampValid ? &mTimestamp : nullptr, mLocal.GetTimestamp());
 
-    if (isNetworkkeyUpdated || compare > 0)
+    if (isNetworkKeyUpdated || compare > 0)
     {
         SuccessOrExit(error = mLocal.Save(aDataset));
 
diff --git a/src/core/meshcop/dtls.cpp b/src/core/meshcop/dtls.cpp
index 88d63a9..28337a6 100644
--- a/src/core/meshcop/dtls.cpp
+++ b/src/core/meshcop/dtls.cpp
@@ -975,7 +975,7 @@
     Error        error   = kErrorNone;
     ot::Message *message = nullptr;
 
-    VerifyOrExit((message = mSocket.NewMessage(0)) != nullptr, error = kErrorNoBufs);
+    VerifyOrExit((message = mSocket.NewMessage()) != nullptr, error = kErrorNoBufs);
     message->SetSubType(aMessageSubType);
     message->SetLinkSecurityEnabled(mLayerTwoSecurity);
 
diff --git a/src/core/meshcop/energy_scan_client.cpp b/src/core/meshcop/energy_scan_client.cpp
index 69ca8ed..905d8a6 100644
--- a/src/core/meshcop/energy_scan_client.cpp
+++ b/src/core/meshcop/energy_scan_client.cpp
@@ -91,7 +91,7 @@
     messageInfo.SetSockAddrToRlocPeerAddrTo(aAddress);
     SuccessOrExit(error = Get<Tmf::Agent>().SendMessage(*message, messageInfo));
 
-    LogInfo("sent query");
+    LogInfo("Sent %s", UriToString<kUriEnergyScan>());
 
     mCallback.Set(aCallback, aContext);
 
@@ -108,7 +108,7 @@
 
     VerifyOrExit(aMessage.IsConfirmablePostRequest());
 
-    LogInfo("received report");
+    LogInfo("Received %s", UriToString<kUriEnergyReport>());
 
     VerifyOrExit((mask = MeshCoP::ChannelMaskTlv::GetChannelMask(aMessage)) != 0);
 
@@ -118,7 +118,7 @@
 
     SuccessOrExit(Get<Tmf::Agent>().SendEmptyAck(aMessage, aMessageInfo));
 
-    LogInfo("sent report response");
+    LogInfo("Sent %s ack", UriToString<kUriEnergyReport>());
 
 exit:
     return;
diff --git a/src/core/meshcop/joiner.cpp b/src/core/meshcop/joiner.cpp
index 21adc7d..84b7d55 100644
--- a/src/core/meshcop/joiner.cpp
+++ b/src/core/meshcop/joiner.cpp
@@ -238,20 +238,8 @@
         aRssi = -127;
     }
 
-    // Limit the RSSI to range (-128, 0), i.e. -128 < aRssi < 0.
-
-    if (aRssi <= -128)
-    {
-        priority = -127;
-    }
-    else if (aRssi >= 0)
-    {
-        priority = -1;
-    }
-    else
-    {
-        priority = aRssi;
-    }
+    // Clamp the RSSI to range [-127, -1]
+    priority = Clamp<int8_t>(aRssi, -127, -1);
 
     // Assign higher priority to networks with an exact match of Joiner
     // ID in the Steering Data (128 < priority < 256) compared to ones
@@ -407,7 +395,7 @@
     {
         SetState(kStateConnected);
         SendJoinerFinalize();
-        mTimer.Start(kReponseTimeout);
+        mTimer.Start(kResponseTimeout);
     }
     else
     {
@@ -486,7 +474,7 @@
     SuccessOrExit(Get<Tmf::SecureAgent>().SendMessage(*mFinalizeMessage, Joiner::HandleJoinerFinalizeResponse, this));
     mFinalizeMessage = nullptr;
 
-    LogInfo("Joiner sent finalize");
+    LogInfo("Sent %s", UriToString<kUriJoinerFinalize>());
 
 exit:
     return;
@@ -515,9 +503,9 @@
     SuccessOrExit(Tlv::Find<StateTlv>(*aMessage, state));
 
     SetState(kStateEntrust);
-    mTimer.Start(kReponseTimeout);
+    mTimer.Start(kResponseTimeout);
 
-    LogInfo("Joiner received finalize response %d", state);
+    LogInfo("Received %s %d", UriToString<kUriJoinerFinalize>(), state);
 
 #if OPENTHREAD_CONFIG_REFERENCE_DEVICE_ENABLE
     LogCertMessage("[THCI] direction=recv | type=JOIN_FIN.rsp |", *aMessage);
@@ -535,7 +523,7 @@
 
     VerifyOrExit(mState == kStateEntrust && aMessage.IsConfirmablePostRequest(), error = kErrorDrop);
 
-    LogInfo("Joiner received entrust");
+    LogInfo("Received %s", UriToString<kUriJoinerEntrust>());
     LogCert("[THCI] direction=recv | type=JOIN_ENT.ntf");
 
     datasetInfo.Clear();
@@ -574,7 +562,7 @@
 
     SetState(kStateJoined);
 
-    LogInfo("Joiner sent entrust response");
+    LogInfo("Sent %s response", UriToString<kUriJoinerEntrust>());
     LogCert("[THCI] direction=send | type=JOIN_ENT.rsp");
 
 exit:
diff --git a/src/core/meshcop/joiner.hpp b/src/core/meshcop/joiner.hpp
index f47dddb..81e4b2b 100644
--- a/src/core/meshcop/joiner.hpp
+++ b/src/core/meshcop/joiner.hpp
@@ -185,7 +185,7 @@
     static constexpr uint16_t kJoinerUdpPort = OPENTHREAD_CONFIG_JOINER_UDP_PORT;
 
     static constexpr uint32_t kConfigExtAddressDelay = 100;  // in msec.
-    static constexpr uint32_t kReponseTimeout        = 4000; ///< Max wait time to receive response (in msec).
+    static constexpr uint32_t kResponseTimeout       = 4000; ///< Max wait time to receive response (in msec).
 
     struct JoinerRouter
     {
diff --git a/src/core/meshcop/joiner_router.cpp b/src/core/meshcop/joiner_router.cpp
index 04741e0..0961602 100644
--- a/src/core/meshcop/joiner_router.cpp
+++ b/src/core/meshcop/joiner_router.cpp
@@ -155,7 +155,7 @@
 
     SuccessOrExit(error = Get<Tmf::Agent>().SendMessage(*message, messageInfo));
 
-    LogInfo("Sent relay rx");
+    LogInfo("Sent %s", UriToString<kUriRelayRx>());
 
 exit:
     FreeMessageOnError(message, error);
@@ -177,7 +177,7 @@
 
     VerifyOrExit(aMessage.IsNonConfirmablePostRequest(), error = kErrorDrop);
 
-    LogInfo("Received relay transmit");
+    LogInfo("Received %s", UriToString<kUriRelayTx>());
 
     SuccessOrExit(error = Tlv::Find<JoinerUdpPortTlv>(aMessage, joinerPort));
     SuccessOrExit(error = Tlv::Find<JoinerIidTlv>(aMessage, joinerIid));
@@ -273,11 +273,10 @@
 
     IgnoreError(Get<Tmf::Agent>().AbortTransaction(&JoinerRouter::HandleJoinerEntrustResponse, this));
 
-    LogInfo("Sending JOIN_ENT.ntf");
     SuccessOrExit(error = Get<Tmf::Agent>().SendMessage(*message, aMessageInfo,
                                                         &JoinerRouter::HandleJoinerEntrustResponse, this));
 
-    LogInfo("Sent joiner entrust length = %d", message->GetLength());
+    LogInfo("Sent %s (len= %d)", UriToString<kUriJoinerEntrust>(), message->GetLength());
     LogCert("[THCI] direction=send | type=JOIN_ENT.ntf");
 
 exit:
@@ -382,7 +381,7 @@
 
     VerifyOrExit(aMessage->GetCode() == Coap::kCodeChanged);
 
-    LogInfo("Receive joiner entrust response");
+    LogInfo("Receive %s response", UriToString<kUriJoinerEntrust>());
     LogCert("[THCI] direction=recv | type=JOIN_ENT.rsp");
 
 exit:
diff --git a/src/core/meshcop/meshcop_leader.cpp b/src/core/meshcop/meshcop_leader.cpp
index 8c24179..5f97185 100644
--- a/src/core/meshcop/meshcop_leader.cpp
+++ b/src/core/meshcop/meshcop_leader.cpp
@@ -71,7 +71,7 @@
     CommissionerIdTlv commissionerId;
     StateTlv::State   state = StateTlv::kReject;
 
-    LogInfo("received petition");
+    LogInfo("Received %s", UriToString<kUriLeaderPetition>());
 
     VerifyOrExit(Get<Mle::MleRouter>().IsRoutingLocator(aMessageInfo.GetPeerAddr()));
     SuccessOrExit(Tlv::FindTlv(aMessage, commissionerId));
@@ -136,7 +136,7 @@
 
     SuccessOrExit(error = Get<Tmf::Agent>().SendMessage(*message, aMessageInfo));
 
-    LogInfo("sent petition response");
+    LogInfo("Sent %s response", UriToString<kUriLeaderPetition>());
 
 exit:
     FreeMessageOnError(message, error);
@@ -150,7 +150,7 @@
     BorderAgentLocatorTlv *borderAgentLocator;
     StateTlv::State        responseState;
 
-    LogInfo("received keep alive");
+    LogInfo("Received %s", UriToString<kUriLeaderKeepAlive>());
 
     SuccessOrExit(Tlv::Find<StateTlv>(aMessage, state));
 
@@ -202,7 +202,7 @@
 
     SuccessOrExit(error = Get<Tmf::Agent>().SendMessage(*message, aMessageInfo));
 
-    LogInfo("sent keep alive response");
+    LogInfo("Sent %s response", UriToString<kUriLeaderKeepAlive>());
 
 exit:
     FreeMessageOnError(message, error);
@@ -221,7 +221,7 @@
     messageInfo.SetSockAddrToRlocPeerAddrTo(aAddress);
     SuccessOrExit(error = Get<Tmf::Agent>().SendMessage(*message, messageInfo));
 
-    LogInfo("sent dataset changed");
+    LogInfo("Sent %s", UriToString<kUriDatasetChanged>());
 
 exit:
     FreeMessageOnError(message, error);
diff --git a/src/core/meshcop/meshcop_tlvs.hpp b/src/core/meshcop/meshcop_tlvs.hpp
index fac5836..1cd2755 100644
--- a/src/core/meshcop/meshcop_tlvs.hpp
+++ b/src/core/meshcop/meshcop_tlvs.hpp
@@ -124,7 +124,7 @@
      * Max length of Provisioning URL TLV.
      *
      */
-    static constexpr uint8_t kMaxkProvisioningUrlLength = OT_PROVISIONING_URL_MAX_SIZE;
+    static constexpr uint8_t kMaxProvisioningUrlLength = OT_PROVISIONING_URL_MAX_SIZE;
 
     static constexpr uint8_t kMaxVendorNameLength      = 32; ///< Max length of Vendor Name TLV.
     static constexpr uint8_t kMaxVendorModelLength     = 32; ///< Max length of Vendor Model TLV.
@@ -1559,7 +1559,7 @@
  * This class defines Provisioning TLV constants and types.
  *
  */
-typedef StringTlvInfo<Tlv::kProvisioningUrl, Tlv::kMaxkProvisioningUrlLength> ProvisioningUrlTlv;
+typedef StringTlvInfo<Tlv::kProvisioningUrl, Tlv::kMaxProvisioningUrlLength> ProvisioningUrlTlv;
 
 /**
  * This class defines Vendor Name TLV constants and types.
diff --git a/src/core/meshcop/network_name.cpp b/src/core/meshcop/network_name.cpp
index 4e0fe48..4316631 100644
--- a/src/core/meshcop/network_name.cpp
+++ b/src/core/meshcop/network_name.cpp
@@ -40,12 +40,6 @@
 namespace ot {
 namespace MeshCoP {
 
-const char NetworkNameManager::sNetworkNameInit[] = "OpenThread";
-
-#if (OPENTHREAD_CONFIG_THREAD_VERSION >= OT_THREAD_VERSION_1_2)
-const char NetworkNameManager::sDomainNameInit[] = "DefaultDomain";
-#endif
-
 uint8_t NameData::CopyTo(char *aBuffer, uint8_t aMaxSize) const
 {
     MutableData<kWithUint8Length> destData;
@@ -114,10 +108,10 @@
 NetworkNameManager::NetworkNameManager(Instance &aInstance)
     : InstanceLocator(aInstance)
 {
-    IgnoreError(SetNetworkName(sNetworkNameInit));
+    IgnoreError(SetNetworkName(NetworkName::kNetworkNameInit));
 
 #if (OPENTHREAD_CONFIG_THREAD_VERSION >= OT_THREAD_VERSION_1_2)
-    IgnoreError(SetDomainName(sDomainNameInit));
+    IgnoreError(SetDomainName(NetworkName::kDomainNameInit));
 #endif
 }
 
diff --git a/src/core/meshcop/network_name.hpp b/src/core/meshcop/network_name.hpp
index 1f6d847..d376fc5 100644
--- a/src/core/meshcop/network_name.hpp
+++ b/src/core/meshcop/network_name.hpp
@@ -107,6 +107,9 @@
 class NetworkName : public otNetworkName, public Unequatable<NetworkName>
 {
 public:
+    static constexpr const char *kNetworkNameInit = "OpenThread";
+    static constexpr const char *kDomainNameInit  = "DefaultDomain";
+
     /**
      * This constant specified the maximum number of chars in Network Name (excludes null char).
      *
@@ -262,9 +265,6 @@
 private:
     Error SignalNetworkNameChange(Error aError);
 
-    static const char sNetworkNameInit[];
-    static const char sDomainNameInit[];
-
     NetworkName mNetworkName;
 
 #if (OPENTHREAD_CONFIG_THREAD_VERSION >= OT_THREAD_VERSION_1_2)
diff --git a/src/core/meshcop/panid_query_client.cpp b/src/core/meshcop/panid_query_client.cpp
index b80ff4e..8c6a004 100644
--- a/src/core/meshcop/panid_query_client.cpp
+++ b/src/core/meshcop/panid_query_client.cpp
@@ -85,7 +85,7 @@
     messageInfo.SetSockAddrToRlocPeerAddrTo(aAddress);
     SuccessOrExit(error = Get<Tmf::Agent>().SendMessage(*message, messageInfo));
 
-    LogInfo("sent panid query");
+    LogInfo("Sent %s", UriToString<kUriPanIdQuery>());
 
     mCallback.Set(aCallback, aContext);
 
@@ -97,13 +97,12 @@
 template <>
 void PanIdQueryClient::HandleTmf<kUriPanIdConflict>(Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo)
 {
-    uint16_t         panId;
-    Ip6::MessageInfo responseInfo(aMessageInfo);
-    uint32_t         mask;
+    uint16_t panId;
+    uint32_t mask;
 
     VerifyOrExit(aMessage.IsConfirmablePostRequest());
 
-    LogInfo("received panid conflict");
+    LogInfo("Received %s", UriToString<kUriPanIdConflict>());
 
     SuccessOrExit(Tlv::Find<MeshCoP::PanIdTlv>(aMessage, panId));
 
@@ -111,9 +110,9 @@
 
     mCallback.InvokeIfSet(panId, mask);
 
-    SuccessOrExit(Get<Tmf::Agent>().SendEmptyAck(aMessage, responseInfo));
+    SuccessOrExit(Get<Tmf::Agent>().SendEmptyAck(aMessage, aMessageInfo));
 
-    LogInfo("sent panid query conflict response");
+    LogInfo("Sent %s response", UriToString<kUriPanIdConflict>());
 
 exit:
     return;
diff --git a/src/core/mtd.cmake b/src/core/mtd.cmake
index 9857a73..e9b831c 100644
--- a/src/core/mtd.cmake
+++ b/src/core/mtd.cmake
@@ -47,6 +47,4 @@
         ot-config
 )
 
-if(NOT OT_EXCLUDE_TCPLP_LIB)
-    target_link_libraries(openthread-mtd PRIVATE tcplp-mtd)
-endif()
+target_link_libraries(openthread-mtd PRIVATE tcplp-mtd)
diff --git a/src/core/net/checksum.cpp b/src/core/net/checksum.cpp
index 6c9d3af..df761d4 100644
--- a/src/core/net/checksum.cpp
+++ b/src/core/net/checksum.cpp
@@ -125,7 +125,7 @@
     uint16_t       length = aMessage.GetLength() - aMessage.GetOffset();
 
     // Pseudo-header for checksum calculation (RFC-768/792/793).
-    // Note: ICMP checksum won't count the presudo header like TCP and UDP.
+    // Note: ICMP checksum won't count the pseudo header like TCP and UDP.
     if (aIpProto != Ip4::kProtoIcmp)
     {
         AddData(aSource.GetBytes(), sizeof(Ip4::Address));
diff --git a/src/core/net/dhcp6.hpp b/src/core/net/dhcp6.hpp
index 6a1f3da..5d65f21 100644
--- a/src/core/net/dhcp6.hpp
+++ b/src/core/net/dhcp6.hpp
@@ -480,7 +480,7 @@
 {
 public:
     static constexpr uint32_t kDefaultPreferredLifetime = 0xffffffffU; ///< Default preferred lifetime.
-    static constexpr uint32_t kDefaultValidLiftetime    = 0xffffffffU; ///< Default valid lifetime.
+    static constexpr uint32_t kDefaultValidLifetime     = 0xffffffffU; ///< Default valid lifetime.
 
     /**
      * This method initializes the DHCPv6 Option.
diff --git a/src/core/net/dhcp6_client.cpp b/src/core/net/dhcp6_client.cpp
index ff3f5a3..db67135 100644
--- a/src/core/net/dhcp6_client.cpp
+++ b/src/core/net/dhcp6_client.cpp
@@ -263,7 +263,7 @@
     Message         *message;
     Ip6::MessageInfo messageInfo;
 
-    VerifyOrExit((message = mSocket.NewMessage(0)) != nullptr, error = kErrorNoBufs);
+    VerifyOrExit((message = mSocket.NewMessage()) != nullptr, error = kErrorNoBufs);
 
     SuccessOrExit(error = AppendHeader(*message));
     SuccessOrExit(error = AppendElapsedTime(*message));
diff --git a/src/core/net/dhcp6_server.cpp b/src/core/net/dhcp6_server.cpp
index 5b66f15..826d5fe 100644
--- a/src/core/net/dhcp6_server.cpp
+++ b/src/core/net/dhcp6_server.cpp
@@ -339,7 +339,7 @@
     Ip6::MessageInfo messageInfo;
     Message         *message;
 
-    VerifyOrExit((message = mSocket.NewMessage(0)) != nullptr, error = kErrorNoBufs);
+    VerifyOrExit((message = mSocket.NewMessage()) != nullptr, error = kErrorNoBufs);
     SuccessOrExit(error = AppendHeader(*message, aTransactionId));
     SuccessOrExit(error = AppendServerIdentifier(*message));
     SuccessOrExit(error = AppendClientIdentifier(*message, aClientId));
@@ -470,7 +470,7 @@
     option.GetAddress().SetPrefix(aPrefix.mFields.m8, OT_IP6_PREFIX_BITSIZE);
     option.GetAddress().GetIid().SetFromExtAddress(aClientId.GetDuidLinkLayerAddress());
     option.SetPreferredLifetime(IaAddress::kDefaultPreferredLifetime);
-    option.SetValidLifetime(IaAddress::kDefaultValidLiftetime);
+    option.SetValidLifetime(IaAddress::kDefaultValidLifetime);
     SuccessOrExit(error = aMessage.Append(option));
 
 exit:
diff --git a/src/core/net/dns_client.cpp b/src/core/net/dns_client.cpp
index e25732e..2dd9531 100644
--- a/src/core/net/dns_client.cpp
+++ b/src/core/net/dns_client.cpp
@@ -70,19 +70,27 @@
     SetResponseTimeout(kDefaultResponseTimeout);
     SetMaxTxAttempts(kDefaultMaxTxAttempts);
     SetRecursionFlag(kDefaultRecursionDesired ? kFlagRecursionDesired : kFlagNoRecursion);
+    SetServiceMode(kDefaultServiceMode);
 #if OPENTHREAD_CONFIG_DNS_CLIENT_NAT64_ENABLE
     SetNat64Mode(kDefaultNat64Allowed ? kNat64Allow : kNat64Disallow);
 #endif
     SetTransportProto(kDnsTransportUdp);
 }
 
-void Client::QueryConfig::SetFrom(const QueryConfig &aConfig, const QueryConfig &aDefaultConfig)
+void Client::QueryConfig::SetFrom(const QueryConfig *aConfig, const QueryConfig &aDefaultConfig)
 {
     // This method sets the config from `aConfig` replacing any
     // unspecified fields (value zero) with the fields from
-    // `aDefaultConfig`.
+    // `aDefaultConfig`. If `aConfig` is `nullptr` then
+    // `aDefaultConfig` is used.
 
-    *this = aConfig;
+    if (aConfig == nullptr)
+    {
+        *this = aDefaultConfig;
+        ExitNow();
+    }
+
+    *this = *aConfig;
 
     if (GetServerSockAddr().GetAddress().IsUnspecified())
     {
@@ -115,10 +123,19 @@
         SetNat64Mode(aDefaultConfig.GetNat64Mode());
     }
 #endif
+
+    if (GetServiceMode() == kServiceModeUnspecified)
+    {
+        SetServiceMode(aDefaultConfig.GetServiceMode());
+    }
+
     if (GetTransportProto() == kDnsTransportUnspecified)
     {
         SetTransportProto(aDefaultConfig.GetTransportProto());
     }
+
+exit:
+    return;
 }
 
 //---------------------------------------------------------------------------------------------------------------------
@@ -230,22 +247,42 @@
 
 #if OPENTHREAD_CONFIG_DNS_CLIENT_SERVICE_DISCOVERY_ENABLE
 
-Error Client::Response::FindServiceInfo(Section aSection, const Name &aName, ServiceInfo &aServiceInfo) const
+void Client::Response::InitServiceInfo(ServiceInfo &aServiceInfo) const
 {
-    // This method searches for SRV and TXT records in the given
-    // section matching the record name against `aName`, and updates
-    // the `aServiceInfo` accordingly. It also searches for AAAA
-    // record for host name associated with the service (from SRV
-    // record). The search for AAAA record is always performed in
-    // Additional Data section (independent of the value given in
-    // `aSection`).
+    // This method initializes `aServiceInfo` setting all
+    // TTLs to zero and host name to empty string.
 
-    Error     error;
+    aServiceInfo.mTtl              = 0;
+    aServiceInfo.mHostAddressTtl   = 0;
+    aServiceInfo.mTxtDataTtl       = 0;
+    aServiceInfo.mTxtDataTruncated = false;
+
+    AsCoreType(&aServiceInfo.mHostAddress).Clear();
+
+    if ((aServiceInfo.mHostNameBuffer != nullptr) && (aServiceInfo.mHostNameBufferSize > 0))
+    {
+        aServiceInfo.mHostNameBuffer[0] = '\0';
+    }
+}
+
+Error Client::Response::ReadServiceInfo(Section aSection, const Name &aName, ServiceInfo &aServiceInfo) const
+{
+    // This method searches for SRV record in the given `aSection`
+    // matching the record name against `aName`, and updates the
+    // `aServiceInfo` accordingly. It also searches for AAAA record
+    // for host name associated with the service (from SRV record).
+    // The search for AAAA record is always performed in Additional
+    // Data section (independent of the value given in `aSection`).
+
+    Error     error = kErrorNone;
     uint16_t  offset;
     uint16_t  numRecords;
     Name      hostName;
     SrvRecord srvRecord;
-    TxtRecord txtRecord;
+
+    // A non-zero `mTtl` indicates that SRV record is already found
+    // and parsed from a previous response.
+    VerifyOrExit(aServiceInfo.mTtl == 0);
 
     VerifyOrExit(mMessage != nullptr, error = kErrorNotFound);
 
@@ -272,61 +309,104 @@
 
     // Search in additional section for AAAA record for the host name.
 
+    VerifyOrExit(AsCoreType(&aServiceInfo.mHostAddress).IsUnspecified());
+
     error = FindHostAddress(kAdditionalDataSection, hostName, /* aIndex */ 0, AsCoreType(&aServiceInfo.mHostAddress),
                             aServiceInfo.mHostAddressTtl);
 
     if (error == kErrorNotFound)
     {
-        AsCoreType(&aServiceInfo.mHostAddress).Clear();
-        aServiceInfo.mHostAddressTtl = 0;
-        error                        = kErrorNone;
-    }
-
-    SuccessOrExit(error);
-
-    // A null `mTxtData` indicates that caller does not want to retrieve TXT data.
-    VerifyOrExit(aServiceInfo.mTxtData != nullptr);
-
-    // Search for a matching TXT record. If not found, indicate this by
-    // setting `aServiceInfo.mTxtDataSize` to zero.
-
-    SelectSection(aSection, offset, numRecords);
-
-    aServiceInfo.mTxtDataTruncated = false;
-
-    error = ResourceRecord::FindRecord(*mMessage, offset, numRecords, /* aIndex */ 0, aName, txtRecord);
-
-    switch (error)
-    {
-    case kErrorNone:
-        error = txtRecord.ReadTxtData(*mMessage, offset, aServiceInfo.mTxtData, aServiceInfo.mTxtDataSize);
-
-        if (error == kErrorNoBufs)
-        {
-            error                          = kErrorNone;
-            aServiceInfo.mTxtDataTruncated = true;
-        }
-
-        SuccessOrExit(error);
-        aServiceInfo.mTxtDataTtl = txtRecord.GetTtl();
-        break;
-
-    case kErrorNotFound:
-        aServiceInfo.mTxtDataSize = 0;
-        aServiceInfo.mTxtDataTtl  = 0;
-        error                     = kErrorNone;
-        break;
-
-    default:
-        ExitNow();
+        error = kErrorNone;
     }
 
 exit:
     return error;
 }
 
+Error Client::Response::ReadTxtRecord(Section aSection, const Name &aName, ServiceInfo &aServiceInfo) const
+{
+    // This method searches a TXT record in the given `aSection`
+    // matching the record name against `aName` and updates the TXT
+    // related properties in `aServicesInfo`.
+    //
+    // If no match is found `mTxtDataTtl` (which is initialized to zero)
+    // remains unchanged to indicate this. In this case this method still
+    // returns `kErrorNone`.
+
+    Error     error = kErrorNone;
+    uint16_t  offset;
+    uint16_t  numRecords;
+    TxtRecord txtRecord;
+
+    // A non-zero `mTxtDataTtl` indicates that TXT record is already
+    // found and parsed from a previous response.
+    VerifyOrExit(aServiceInfo.mTxtDataTtl == 0);
+
+    // A null `mTxtData` indicates that caller does not want to retrieve
+    // TXT data.
+    VerifyOrExit(aServiceInfo.mTxtData != nullptr);
+
+    VerifyOrExit(mMessage != nullptr, error = kErrorNotFound);
+
+    SelectSection(aSection, offset, numRecords);
+
+    aServiceInfo.mTxtDataTruncated = false;
+
+    SuccessOrExit(error = ResourceRecord::FindRecord(*mMessage, offset, numRecords, /* aIndex */ 0, aName, txtRecord));
+
+    error = txtRecord.ReadTxtData(*mMessage, offset, aServiceInfo.mTxtData, aServiceInfo.mTxtDataSize);
+
+    if (error == kErrorNoBufs)
+    {
+        error = kErrorNone;
+
+        // Mark `mTxtDataTruncated` to indicate that we could not read
+        // the full TXT record into the given `mTxtData` buffer.
+        aServiceInfo.mTxtDataTruncated = true;
+    }
+
+    SuccessOrExit(error);
+    aServiceInfo.mTxtDataTtl = txtRecord.GetTtl();
+
+exit:
+    if (error == kErrorNotFound)
+    {
+        error = kErrorNone;
+    }
+
+    return error;
+}
+
 #endif // OPENTHREAD_CONFIG_DNS_CLIENT_SERVICE_DISCOVERY_ENABLE
 
+void Client::Response::PopulateFrom(const Message &aMessage)
+{
+    // Populate `Response` with info from `aMessage`.
+
+    uint16_t offset = aMessage.GetOffset();
+    Header   header;
+
+    mMessage = &aMessage;
+
+    IgnoreError(aMessage.Read(offset, header));
+    offset += sizeof(Header);
+
+    for (uint16_t num = 0; num < header.GetQuestionCount(); num++)
+    {
+        IgnoreError(Name::ParseName(aMessage, offset));
+        offset += sizeof(Question);
+    }
+
+    mAnswerOffset = offset;
+    IgnoreError(ResourceRecord::ParseRecords(aMessage, offset, header.GetAnswerCount()));
+    IgnoreError(ResourceRecord::ParseRecords(aMessage, offset, header.GetAuthorityRecordCount()));
+    mAdditionalOffset = offset;
+    IgnoreError(ResourceRecord::ParseRecords(aMessage, offset, header.GetAdditionalRecordCount()));
+
+    mAnswerRecordCount     = header.GetAnswerCount();
+    mAdditionalRecordCount = header.GetAdditionalRecordCount();
+}
+
 //---------------------------------------------------------------------------------------------------------------------
 // Client::AddressResponse
 
@@ -400,12 +480,20 @@
     Error error;
     Name  instanceName;
 
-    // Find a matching PTR record for the service instance label.
-    // Then search and read SRV, TXT and AAAA records in Additional Data section
-    // matching the same name to populate `aServiceInfo`.
+    // Find a matching PTR record for the service instance label. Then
+    // search and read SRV, TXT and AAAA records in Additional Data
+    // section matching the same name to populate `aServiceInfo`.
 
     SuccessOrExit(error = FindPtrRecord(aInstanceLabel, instanceName));
-    error = FindServiceInfo(kAdditionalDataSection, instanceName, aServiceInfo);
+
+    InitServiceInfo(aServiceInfo);
+    SuccessOrExit(error = ReadServiceInfo(kAdditionalDataSection, instanceName, aServiceInfo));
+    SuccessOrExit(error = ReadTxtRecord(kAdditionalDataSection, instanceName, aServiceInfo));
+
+    if (aServiceInfo.mTxtDataTtl == 0)
+    {
+        aServiceInfo.mTxtDataSize = 0;
+    }
 
 exit:
     return error;
@@ -497,10 +585,72 @@
 
 Error Client::ServiceResponse::GetServiceInfo(ServiceInfo &aServiceInfo) const
 {
-    // Search and read SRV, TXT records in Answer Section
-    // matching name from query.
+    // Search and read SRV, TXT records matching name from query.
 
-    return FindServiceInfo(kAnswerSection, Name(*mQuery, kNameOffsetInQuery), aServiceInfo);
+    Error error = kErrorNotFound;
+
+    InitServiceInfo(aServiceInfo);
+
+    for (const Response *response = this; response != nullptr; response = response->mNext)
+    {
+        Name      name(*response->mQuery, kNameOffsetInQuery);
+        QueryInfo info;
+        Section   srvSection;
+        Section   txtSection;
+
+        info.ReadFrom(*response->mQuery);
+
+        switch (info.mQueryType)
+        {
+        case kIp6AddressQuery:
+#if OPENTHREAD_CONFIG_DNS_CLIENT_NAT64_ENABLE
+        case kIp4AddressQuery:
+#endif
+            IgnoreError(response->FindHostAddress(kAnswerSection, name, /* aIndex */ 0,
+                                                  AsCoreType(&aServiceInfo.mHostAddress),
+                                                  aServiceInfo.mHostAddressTtl));
+
+            continue; // to `for()` loop
+
+        case kServiceQuerySrvTxt:
+        case kServiceQuerySrv:
+        case kServiceQueryTxt:
+            break;
+
+        default:
+            continue;
+        }
+
+        // Determine from which section we should try to read the SRV and
+        // TXT records based on the query type.
+        //
+        // In `kServiceQuerySrv` or `kServiceQueryTxt` we expect to see
+        // only one record (SRV or TXT) in the answer section, but we
+        // still try to read the other records from additional data
+        // section in case server provided them.
+
+        srvSection = (info.mQueryType != kServiceQueryTxt) ? kAnswerSection : kAdditionalDataSection;
+        txtSection = (info.mQueryType != kServiceQuerySrv) ? kAnswerSection : kAdditionalDataSection;
+
+        error = response->ReadServiceInfo(srvSection, name, aServiceInfo);
+
+        if ((srvSection == kAdditionalDataSection) && (error == kErrorNotFound))
+        {
+            error = kErrorNone;
+        }
+
+        SuccessOrExit(error);
+
+        SuccessOrExit(error = response->ReadTxtRecord(txtSection, name, aServiceInfo));
+    }
+
+    if (aServiceInfo.mTxtDataTtl == 0)
+    {
+        aServiceInfo.mTxtDataSize = 0;
+    }
+
+exit:
+    return error;
 }
 
 Error Client::ServiceResponse::GetHostAddress(const char   *aHostName,
@@ -508,7 +658,37 @@
                                               Ip6::Address &aAddress,
                                               uint32_t     &aTtl) const
 {
-    return FindHostAddress(kAdditionalDataSection, Name(aHostName), aIndex, aAddress, aTtl);
+    Error error = kErrorNotFound;
+
+    for (const Response *response = this; response != nullptr; response = response->mNext)
+    {
+        Section   section = kAdditionalDataSection;
+        QueryInfo info;
+
+        info.ReadFrom(*response->mQuery);
+
+        switch (info.mQueryType)
+        {
+        case kIp6AddressQuery:
+#if OPENTHREAD_CONFIG_DNS_CLIENT_NAT64_ENABLE
+        case kIp4AddressQuery:
+#endif
+            section = kAnswerSection;
+            break;
+
+        default:
+            break;
+        }
+
+        error = response->FindHostAddress(section, Name(aHostName), aIndex, aAddress, aTtl);
+
+        if (error == kErrorNone)
+        {
+            break;
+        }
+    }
+
+    return error;
 }
 
 #endif // OPENTHREAD_CONFIG_DNS_CLIENT_SERVICE_DISCOVERY_ENABLE
@@ -526,24 +706,29 @@
 #endif
 
 const uint8_t Client::kQuestionCount[] = {
-    /* kIp6AddressQuery -> */ GetArrayLength(kIp6AddressQueryRecordTypes), // AAAA records
+    /* kIp6AddressQuery -> */ GetArrayLength(kIp6AddressQueryRecordTypes), // AAAA record
 #if OPENTHREAD_CONFIG_DNS_CLIENT_NAT64_ENABLE
-    /* kIp4AddressQuery -> */ GetArrayLength(kIp4AddressQueryRecordTypes), // A records
+    /* kIp4AddressQuery -> */ GetArrayLength(kIp4AddressQueryRecordTypes), // A record
 #endif
 #if OPENTHREAD_CONFIG_DNS_CLIENT_SERVICE_DISCOVERY_ENABLE
-    /* kBrowseQuery  -> */ GetArrayLength(kBrowseQueryRecordTypes),  // PTR records
-    /* kServiceQuery -> */ GetArrayLength(kServiceQueryRecordTypes), // SRV and TXT records
+    /* kBrowseQuery        -> */ GetArrayLength(kBrowseQueryRecordTypes),  // PTR record
+    /* kServiceQuerySrvTxt -> */ GetArrayLength(kServiceQueryRecordTypes), // SRV and TXT records
+    /* kServiceQuerySrv    -> */ 1,                                        // SRV record only
+    /* kServiceQueryTxt    -> */ 1,                                        // TXT record only
 #endif
 };
 
-const uint16_t *Client::kQuestionRecordTypes[] = {
+const uint16_t *const Client::kQuestionRecordTypes[] = {
     /* kIp6AddressQuery -> */ kIp6AddressQueryRecordTypes,
 #if OPENTHREAD_CONFIG_DNS_CLIENT_NAT64_ENABLE
     /* kIp4AddressQuery -> */ kIp4AddressQueryRecordTypes,
 #endif
 #if OPENTHREAD_CONFIG_DNS_CLIENT_SERVICE_DISCOVERY_ENABLE
     /* kBrowseQuery  -> */ kBrowseQueryRecordTypes,
-    /* kServiceQuery -> */ kServiceQueryRecordTypes,
+    /* kServiceQuerySrvTxt -> */ kServiceQueryRecordTypes,
+    /* kServiceQuerySrv    -> */ &kServiceQueryRecordTypes[0],
+    /* kServiceQueryTxt    -> */ &kServiceQueryRecordTypes[1],
+
 #endif
 };
 
@@ -564,11 +749,15 @@
     static_assert(kIp4AddressQuery == 1, "kIp4AddressQuery value is not correct");
 #if OPENTHREAD_CONFIG_DNS_CLIENT_SERVICE_DISCOVERY_ENABLE
     static_assert(kBrowseQuery == 2, "kBrowseQuery value is not correct");
-    static_assert(kServiceQuery == 3, "kServiceQuery value is not correct");
+    static_assert(kServiceQuerySrvTxt == 3, "kServiceQuerySrvTxt value is not correct");
+    static_assert(kServiceQuerySrv == 4, "kServiceQuerySrv value is not correct");
+    static_assert(kServiceQueryTxt == 5, "kServiceQueryTxt value is not correct");
 #endif
 #elif OPENTHREAD_CONFIG_DNS_CLIENT_SERVICE_DISCOVERY_ENABLE
     static_assert(kBrowseQuery == 1, "kBrowseQuery value is not correct");
-    static_assert(kServiceQuery == 2, "kServiceQuery value is not correct");
+    static_assert(kServiceQuerySrvTxt == 2, "kServiceQuerySrvTxt value is not correct");
+    static_assert(kServiceQuerySrv == 3, "kServiceQuerySrv value is not correct");
+    static_assert(kServiceQueryTxt == 4, "kServiceQuerySrv value is not correct");
 #endif
 }
 
@@ -587,7 +776,7 @@
 {
     Query *query;
 
-    while ((query = mQueries.GetHead()) != nullptr)
+    while ((query = mMainQueries.GetHead()) != nullptr)
     {
         FinalizeQuery(*query, kErrorAbort);
     }
@@ -630,7 +819,7 @@
 {
     QueryConfig startingDefault(QueryConfig::kInitFromDefaults);
 
-    mDefaultConfig.SetFrom(aQueryConfig, startingDefault);
+    mDefaultConfig.SetFrom(&aQueryConfig, startingDefault);
 
 #if OPENTHREAD_CONFIG_DNS_CLIENT_DEFAULT_SERVER_ADDRESS_AUTO_SET_ENABLE
     mUserDidSetDefaultAddress = !aQueryConfig.GetServerSockAddr().GetAddress().IsUnspecified();
@@ -669,10 +858,12 @@
     QueryInfo info;
 
     info.Clear();
-    info.mQueryType                 = kIp6AddressQuery;
+    info.mQueryType = kIp6AddressQuery;
+    info.mConfig.SetFrom(aConfig, mDefaultConfig);
     info.mCallback.mAddressCallback = aCallback;
+    info.mCallbackContext           = aContext;
 
-    return StartQuery(info, aConfig, nullptr, aHostName, aContext);
+    return StartQuery(info, nullptr, aHostName);
 }
 
 #if OPENTHREAD_CONFIG_DNS_CLIENT_NAT64_ENABLE
@@ -684,10 +875,12 @@
     QueryInfo info;
 
     info.Clear();
-    info.mQueryType                 = kIp4AddressQuery;
+    info.mQueryType = kIp4AddressQuery;
+    info.mConfig.SetFrom(aConfig, mDefaultConfig);
     info.mCallback.mAddressCallback = aCallback;
+    info.mCallbackContext           = aContext;
 
-    return StartQuery(info, aConfig, nullptr, aHostName, aContext);
+    return StartQuery(info, nullptr, aHostName);
 }
 #endif
 
@@ -698,10 +891,12 @@
     QueryInfo info;
 
     info.Clear();
-    info.mQueryType                = kBrowseQuery;
+    info.mQueryType = kBrowseQuery;
+    info.mConfig.SetFrom(aConfig, mDefaultConfig);
     info.mCallback.mBrowseCallback = aCallback;
+    info.mCallbackContext          = aContext;
 
-    return StartQuery(info, aConfig, nullptr, aServiceName, aContext);
+    return StartQuery(info, nullptr, aServiceName);
 }
 
 Error Client::ResolveService(const char        *aInstanceLabel,
@@ -710,16 +905,63 @@
                              void              *aContext,
                              const QueryConfig *aConfig)
 {
+    return Resolve(aInstanceLabel, aServiceName, aCallback, aContext, aConfig, false);
+}
+
+Error Client::ResolveServiceAndHostAddress(const char        *aInstanceLabel,
+                                           const char        *aServiceName,
+                                           ServiceCallback    aCallback,
+                                           void              *aContext,
+                                           const QueryConfig *aConfig)
+{
+    return Resolve(aInstanceLabel, aServiceName, aCallback, aContext, aConfig, true);
+}
+
+Error Client::Resolve(const char        *aInstanceLabel,
+                      const char        *aServiceName,
+                      ServiceCallback    aCallback,
+                      void              *aContext,
+                      const QueryConfig *aConfig,
+                      bool               aShouldResolveHostAddr)
+{
     QueryInfo info;
     Error     error;
+    QueryType secondQueryType = kNoQuery;
 
     VerifyOrExit(aInstanceLabel != nullptr, error = kErrorInvalidArgs);
 
     info.Clear();
-    info.mQueryType                 = kServiceQuery;
-    info.mCallback.mServiceCallback = aCallback;
 
-    error = StartQuery(info, aConfig, aInstanceLabel, aServiceName, aContext);
+    info.mConfig.SetFrom(aConfig, mDefaultConfig);
+    info.mShouldResolveHostAddr = aShouldResolveHostAddr;
+
+    switch (info.mConfig.GetServiceMode())
+    {
+    case QueryConfig::kServiceModeSrvTxtSeparate:
+        secondQueryType = kServiceQueryTxt;
+
+        OT_FALL_THROUGH;
+
+    case QueryConfig::kServiceModeSrv:
+        info.mQueryType = kServiceQuerySrv;
+        break;
+
+    case QueryConfig::kServiceModeTxt:
+        info.mQueryType = kServiceQueryTxt;
+        VerifyOrExit(!info.mShouldResolveHostAddr, error = kErrorInvalidArgs);
+        break;
+
+    case QueryConfig::kServiceModeSrvTxt:
+    case QueryConfig::kServiceModeSrvTxtOptimize:
+    default:
+        info.mQueryType = kServiceQuerySrvTxt;
+        break;
+    }
+
+    info.mCallback.mServiceCallback = aCallback;
+    info.mCallbackContext           = aContext;
+
+    error = StartQuery(info, aInstanceLabel, aServiceName, secondQueryType);
 
 exit:
     return error;
@@ -727,37 +969,17 @@
 
 #endif // OPENTHREAD_CONFIG_DNS_CLIENT_SERVICE_DISCOVERY_ENABLE
 
-Error Client::StartQuery(QueryInfo         &aInfo,
-                         const QueryConfig *aConfig,
-                         const char        *aLabel,
-                         const char        *aName,
-                         void              *aContext)
+Error Client::StartQuery(QueryInfo &aInfo, const char *aLabel, const char *aName, QueryType aSecondType)
 {
-    // This method assumes that `mQueryType` and `mCallback` to be
-    // already set by caller on `aInfo`. The `aLabel` can be `nullptr`
-    // and then `aName` provides the full name, otherwise the name is
-    // appended as `{aLabel}.{aName}`.
+    // The `aLabel` can be `nullptr` and then `aName` provides the
+    // full name, otherwise the name is appended as `{aLabel}.
+    // {aName}`.
 
     Error  error;
     Query *query;
 
     VerifyOrExit(mSocket.IsBound(), error = kErrorInvalidState);
 
-    if (aConfig == nullptr)
-    {
-        aInfo.mConfig = mDefaultConfig;
-    }
-    else
-    {
-        // To form the config for this query, replace any unspecified
-        // fields (zero value) in the given `aConfig` with the fields
-        // from `mDefaultConfig`.
-
-        aInfo.mConfig.SetFrom(*aConfig, mDefaultConfig);
-    }
-
-    aInfo.mCallbackContext = aContext;
-
 #if OPENTHREAD_CONFIG_DNS_CLIENT_NAT64_ENABLE
     if (aInfo.mQueryType == kIp4AddressQuery)
     {
@@ -770,10 +992,33 @@
 #endif
 
     SuccessOrExit(error = AllocateQuery(aInfo, aLabel, aName, query));
-    mQueries.Enqueue(*query);
-    if ((error = SendQuery(*query, aInfo, /* aUpdateTimer */ true)) != kErrorNone)
+
+    mMainQueries.Enqueue(*query);
+
+    error = SendQuery(*query, aInfo, /* aUpdateTimer */ true);
+    VerifyOrExit(error == kErrorNone, FreeQuery(*query));
+
+    if (aSecondType != kNoQuery)
     {
-        FreeQuery(*query);
+        Query *secondQuery;
+
+        aInfo.mQueryType         = aSecondType;
+        aInfo.mMessageId         = 0;
+        aInfo.mTransmissionCount = 0;
+        aInfo.mMainQuery         = query;
+
+        // We intentionally do not use `error` here so in the unlikely
+        // case where we cannot allocate the second query we can proceed
+        // with the first one.
+        SuccessOrExit(AllocateQuery(aInfo, aLabel, aName, secondQuery));
+
+        IgnoreError(SendQuery(*secondQuery, aInfo, /* aUpdateTimer */ true));
+
+        // Update first query to link to second one by updating
+        // its `mNextQuery`.
+        aInfo.ReadFrom(*query);
+        aInfo.mNextQuery = secondQuery;
+        UpdateQuery(*query, aInfo);
     }
 
 exit:
@@ -805,7 +1050,29 @@
     return error;
 }
 
-void Client::FreeQuery(Query &aQuery) { mQueries.DequeueAndFree(aQuery); }
+Client::Query &Client::FindMainQuery(Query &aQuery)
+{
+    QueryInfo info;
+
+    info.ReadFrom(aQuery);
+
+    return (info.mMainQuery == nullptr) ? aQuery : *info.mMainQuery;
+}
+
+void Client::FreeQuery(Query &aQuery)
+{
+    Query    &mainQuery = FindMainQuery(aQuery);
+    QueryInfo info;
+
+    mMainQueries.Dequeue(mainQuery);
+
+    for (Query *query = &mainQuery; query != nullptr; query = info.mNextQuery)
+    {
+        info.ReadFrom(*query);
+        FreeMessage(info.mSavedResponse);
+        query->Free();
+    }
+}
 
 Error Client::SendQuery(Query &aQuery, QueryInfo &aInfo, bool aUpdateTimer)
 {
@@ -849,7 +1116,7 @@
 
     header.SetQuestionCount(kQuestionCount[aInfo.mQueryType]);
 
-    message = mSocket.NewMessage(0);
+    message = mSocket.NewMessage();
     VerifyOrExit(message != nullptr, error = kErrorNoBufs);
 
     SuccessOrExit(error = message->Append(header));
@@ -885,7 +1152,7 @@
             SuccessOrExit(error = InitTcpSocket());
             SuccessOrExit(
                 error = mEndpoint.Connect(AsCoreType(&aInfo.mConfig.mServerSockAddr), OT_TCP_CONNECT_NO_FAST_OPEN));
-            mTcpState = kTcpConecting;
+            mTcpState = kTcpConnecting;
             PrepareTcpMessage(*message);
             break;
         case kTcpConnectedIdle:
@@ -893,7 +1160,7 @@
             SuccessOrExit(error = mEndpoint.SendByReference(mSendLink, /* aFlags */ 0));
             mTcpState = kTcpConnectedSending;
             break;
-        case kTcpConecting:
+        case kTcpConnecting:
             PrepareTcpMessage(*message);
             break;
         case kTcpConnectedSending:
@@ -944,24 +1211,24 @@
 
 void Client::FinalizeQuery(Query &aQuery, Error aError)
 {
-    Response  response;
-    QueryInfo info;
+    Response response;
+    Query   &mainQuery = FindMainQuery(aQuery);
 
     response.mInstance = &Get<Instance>();
-    response.mQuery    = &aQuery;
-    info.ReadFrom(aQuery);
+    response.mQuery    = &mainQuery;
 
-    FinalizeQuery(response, info.mQueryType, aError);
+    FinalizeQuery(response, aError);
 }
 
-void Client::FinalizeQuery(Response &aResponse, QueryType aType, Error aError)
+void Client::FinalizeQuery(Response &aResponse, Error aError)
 {
-    Callback callback;
-    void    *context;
+    QueryType type;
+    Callback  callback;
+    void     *context;
 
-    GetCallback(*aResponse.mQuery, callback, context);
+    GetQueryTypeAndCallback(*aResponse.mQuery, type, callback, context);
 
-    switch (aType)
+    switch (type)
     {
     case kIp6AddressQuery:
 #if OPENTHREAD_CONFIG_DNS_CLIENT_NAT64_ENABLE
@@ -981,24 +1248,29 @@
         }
         break;
 
-    case kServiceQuery:
+    case kServiceQuerySrvTxt:
+    case kServiceQuerySrv:
+    case kServiceQueryTxt:
         if (callback.mServiceCallback != nullptr)
         {
             callback.mServiceCallback(aError, &aResponse, context);
         }
         break;
 #endif
+    case kNoQuery:
+        break;
     }
 
     FreeQuery(*aResponse.mQuery);
 }
 
-void Client::GetCallback(const Query &aQuery, Callback &aCallback, void *&aContext)
+void Client::GetQueryTypeAndCallback(const Query &aQuery, QueryType &aType, Callback &aCallback, void *&aContext)
 {
     QueryInfo info;
 
     info.ReadFrom(aQuery);
 
+    aType     = info.mQueryType;
     aCallback = info.mCallback;
     aContext  = info.mCallbackContext;
 }
@@ -1008,17 +1280,21 @@
     Query    *matchedQuery = nullptr;
     QueryInfo info;
 
-    for (Query &query : mQueries)
+    for (Query &mainQuery : mMainQueries)
     {
-        info.ReadFrom(query);
-
-        if (info.mMessageId == aMessageId)
+        for (Query *query = &mainQuery; query != nullptr; query = info.mNextQuery)
         {
-            matchedQuery = &query;
-            break;
+            info.ReadFrom(*query);
+
+            if (info.mMessageId == aMessageId)
+            {
+                matchedQuery = query;
+                ExitNow();
+            }
         }
     }
 
+exit:
     return matchedQuery;
 }
 
@@ -1029,58 +1305,82 @@
     static_cast<Client *>(aContext)->ProcessResponse(AsCoreType(aMessage));
 }
 
-void Client::ProcessResponse(const Message &aMessage)
+void Client::ProcessResponse(const Message &aResponseMessage)
 {
-    Response  response;
-    QueryType type;
-    Error     responseError;
+    Error  responseError;
+    Query *query;
 
-    response.mInstance = &Get<Instance>();
-    response.mMessage  = &aMessage;
+    SuccessOrExit(ParseResponse(aResponseMessage, query, responseError));
 
-    // We intentionally parse the response in a separate method
-    // `ParseResponse()` to free all the stack allocated variables
-    // (e.g., `QueryInfo`) used during parsing of the message before
-    // finalizing the query and invoking the user's callback.
+    if (responseError != kErrorNone)
+    {
+        // Received an error from server, check if we can replace
+        // the query.
 
-    SuccessOrExit(ParseResponse(response, type, responseError));
-    FinalizeQuery(response, type, responseError);
+#if OPENTHREAD_CONFIG_DNS_CLIENT_NAT64_ENABLE
+        if (ReplaceWithIp4Query(*query) == kErrorNone)
+        {
+            ExitNow();
+        }
+#endif
+#if OPENTHREAD_CONFIG_DNS_CLIENT_SERVICE_DISCOVERY_ENABLE
+        if (ReplaceWithSeparateSrvTxtQueries(*query) == kErrorNone)
+        {
+            ExitNow();
+        }
+#endif
+
+        FinalizeQuery(*query, responseError);
+        ExitNow();
+    }
+
+    // Received successful response from server.
+
+#if OPENTHREAD_CONFIG_DNS_CLIENT_SERVICE_DISCOVERY_ENABLE
+    ResolveHostAddressIfNeeded(*query, aResponseMessage);
+#endif
+
+    if (!CanFinalizeQuery(*query))
+    {
+        SaveQueryResponse(*query, aResponseMessage);
+        ExitNow();
+    }
+
+    PrepareResponseAndFinalize(FindMainQuery(*query), aResponseMessage, nullptr);
 
 exit:
     return;
 }
 
-Error Client::ParseResponse(Response &aResponse, QueryType &aType, Error &aResponseError)
+Error Client::ParseResponse(const Message &aResponseMessage, Query *&aQuery, Error &aResponseError)
 {
-    Error          error   = kErrorNone;
-    const Message &message = *aResponse.mMessage;
-    uint16_t       offset  = message.GetOffset();
-    Header         header;
-    QueryInfo      info;
-    Name           queryName;
+    Error     error  = kErrorNone;
+    uint16_t  offset = aResponseMessage.GetOffset();
+    Header    header;
+    QueryInfo info;
+    Name      queryName;
 
-    SuccessOrExit(error = message.Read(offset, header));
+    SuccessOrExit(error = aResponseMessage.Read(offset, header));
     offset += sizeof(Header);
 
     VerifyOrExit((header.GetType() == Header::kTypeResponse) && (header.GetQueryType() == Header::kQueryTypeStandard) &&
                      !header.IsTruncationFlagSet(),
                  error = kErrorDrop);
 
-    aResponse.mQuery = FindQueryById(header.GetMessageId());
-    VerifyOrExit(aResponse.mQuery != nullptr, error = kErrorNotFound);
+    aQuery = FindQueryById(header.GetMessageId());
+    VerifyOrExit(aQuery != nullptr, error = kErrorNotFound);
 
-    info.ReadFrom(*aResponse.mQuery);
-    aType = info.mQueryType;
+    info.ReadFrom(*aQuery);
 
-    queryName.SetFromMessage(*aResponse.mQuery, kNameOffsetInQuery);
+    queryName.SetFromMessage(*aQuery, kNameOffsetInQuery);
 
     // Check the Question Section
 
-    if (header.GetQuestionCount() == kQuestionCount[aType])
+    if (header.GetQuestionCount() == kQuestionCount[info.mQueryType])
     {
-        for (uint8_t num = 0; num < kQuestionCount[aType]; num++)
+        for (uint8_t num = 0; num < kQuestionCount[info.mQueryType]; num++)
         {
-            SuccessOrExit(error = Name::CompareName(message, offset, queryName));
+            SuccessOrExit(error = Name::CompareName(aResponseMessage, offset, queryName));
             offset += sizeof(Question);
         }
     }
@@ -1092,74 +1392,103 @@
 
     // Check the answer, authority and additional record sections
 
-    aResponse.mAnswerOffset = offset;
-    SuccessOrExit(error = ResourceRecord::ParseRecords(message, offset, header.GetAnswerCount()));
-    SuccessOrExit(error = ResourceRecord::ParseRecords(message, offset, header.GetAuthorityRecordCount()));
-    aResponse.mAdditionalOffset = offset;
-    SuccessOrExit(error = ResourceRecord::ParseRecords(message, offset, header.GetAdditionalRecordCount()));
+    SuccessOrExit(error = ResourceRecord::ParseRecords(aResponseMessage, offset, header.GetAnswerCount()));
+    SuccessOrExit(error = ResourceRecord::ParseRecords(aResponseMessage, offset, header.GetAuthorityRecordCount()));
+    SuccessOrExit(error = ResourceRecord::ParseRecords(aResponseMessage, offset, header.GetAdditionalRecordCount()));
 
-    aResponse.mAnswerRecordCount     = header.GetAnswerCount();
-    aResponse.mAdditionalRecordCount = header.GetAdditionalRecordCount();
-
-    // Check the response code from server
+    // Read the response code
 
     aResponseError = Header::ResponseCodeToError(header.GetResponseCode());
 
-#if OPENTHREAD_CONFIG_DNS_CLIENT_NAT64_ENABLE
+exit:
+    return error;
+}
 
-    if (aType == kIp6AddressQuery)
+bool Client::CanFinalizeQuery(Query &aQuery)
+{
+    // Determines whether we can finalize a main query by checking if
+    // we have received and saved responses for all other related
+    // queries associated with `aQuery`. Note that this method is
+    // called when we receive a response for `aQuery`, so no need to
+    // check for a saved response for `aQuery` itself.
+
+    bool      canFinalize = true;
+    QueryInfo info;
+
+    for (Query *query = &FindMainQuery(aQuery); query != nullptr; query = info.mNextQuery)
     {
-        Ip6::Address ip6ddress;
-        uint32_t     ttl;
-        ARecord      aRecord;
+        info.ReadFrom(*query);
 
-        // If the response does not contain an answer for the IPv6 address
-        // resolution query and if NAT64 is allowed for this query, we can
-        // perform IPv4 to IPv6 address translation.
-
-        VerifyOrExit(aResponse.FindHostAddress(Response::kAnswerSection, queryName, /* aIndex */ 0, ip6ddress, ttl) !=
-                     kErrorNone);
-        VerifyOrExit(info.mConfig.GetNat64Mode() == QueryConfig::kNat64Allow);
-
-        // First, we check if the response already contains an A record
-        // (IPv4 address) for the query name.
-
-        if (aResponse.FindARecord(Response::kAdditionalDataSection, queryName, /* aIndex */ 0, aRecord) == kErrorNone)
+        if (query == &aQuery)
         {
-            aResponse.mIp6QueryResponseRequiresNat64 = true;
-            aResponseError                           = kErrorNone;
-            ExitNow();
+            continue;
         }
 
-        // Otherwise, we send a new query for IPv4 address resolution
-        // for the same host name. We reuse the existing `query`
-        // instance and keep all the info but clear `mTransmissionCount`
-        // and `mMessageId` (so that a new random message ID is
-        // selected). The new `info` will be saved in the query in
-        // `SendQuery()`. Note that the current query is still in the
-        // `mQueries` list when `SendQuery()` selects a new random
-        // message ID, so the existing message ID for this query will
-        // not be reused. Since the query is not yet resolved, we
-        // return `kErrorPending`.
-
-        info.mQueryType         = kIp4AddressQuery;
-        info.mMessageId         = 0;
-        info.mTransmissionCount = 0;
-
-        IgnoreReturnValue(SendQuery(*aResponse.mQuery, info, /* aUpdateTimer */ true));
-
-        error = kErrorPending;
+        if (info.mSavedResponse == nullptr)
+        {
+            canFinalize = false;
+            ExitNow();
+        }
     }
 
-#endif // OPENTHREAD_CONFIG_DNS_CLIENT_NAT64_ENABLE
-
 exit:
-    if (error != kErrorNone)
-    {
-        LogInfo("Failed to parse response %s", ErrorToString(error));
-    }
+    return canFinalize;
+}
 
-    return error;
+void Client::SaveQueryResponse(Query &aQuery, const Message &aResponseMessage)
+{
+    QueryInfo info;
+
+    info.ReadFrom(aQuery);
+    VerifyOrExit(info.mSavedResponse == nullptr);
+
+    // If `Clone()` fails we let retry or timeout handle the error.
+    info.mSavedResponse = aResponseMessage.Clone();
+
+    UpdateQuery(aQuery, info);
+
+exit:
+    return;
+}
+
+Client::Query *Client::PopulateResponse(Response &aResponse, Query &aQuery, const Message &aResponseMessage)
+{
+    // Populate `aResponse` for `aQuery`. If there is a saved response
+    // message for `aQuery` we use it, otherwise, we use
+    // `aResponseMessage`.
+
+    QueryInfo info;
+
+    info.ReadFrom(aQuery);
+
+    aResponse.mInstance = &Get<Instance>();
+    aResponse.mQuery    = &aQuery;
+    aResponse.PopulateFrom((info.mSavedResponse == nullptr) ? aResponseMessage : *info.mSavedResponse);
+
+    return info.mNextQuery;
+}
+
+void Client::PrepareResponseAndFinalize(Query &aQuery, const Message &aResponseMessage, Response *aPrevResponse)
+{
+    // This method prepares a list of chained `Response` instances
+    // corresponding to all related (chained) queries. It uses
+    // recursion to go through the queries and construct the
+    // `Response` chain.
+
+    Response response;
+    Query   *nextQuery;
+
+    nextQuery      = PopulateResponse(response, aQuery, aResponseMessage);
+    response.mNext = aPrevResponse;
+
+    if (nextQuery != nullptr)
+    {
+        PrepareResponseAndFinalize(*nextQuery, aResponseMessage, &response);
+    }
+    else
+    {
+        FinalizeQuery(response, kErrorNone);
+    }
 }
 
 void Client::HandleTimer(void)
@@ -1171,29 +1500,40 @@
     bool hasTcpQuery = false;
 #endif
 
-    for (Query &query : mQueries)
+    for (Query &mainQuery : mMainQueries)
     {
-        info.ReadFrom(query);
-
-        if (now >= info.mRetransmissionTime)
+        for (Query *query = &mainQuery; query != nullptr; query = info.mNextQuery)
         {
-            if (info.mTransmissionCount >= info.mConfig.GetMaxTxAttempts())
+            info.ReadFrom(*query);
+
+            if (info.mSavedResponse != nullptr)
             {
-                FinalizeQuery(query, kErrorResponseTimeout);
                 continue;
             }
 
-            IgnoreReturnValue(SendQuery(query, info, /* aUpdateTimer */ false));
-        }
+            if (now >= info.mRetransmissionTime)
+            {
+                if (info.mTransmissionCount >= info.mConfig.GetMaxTxAttempts())
+                {
+                    FinalizeQuery(*query, kErrorResponseTimeout);
+                    break;
+                }
 
-        nextTime = Min(nextTime, info.mRetransmissionTime);
+                IgnoreError(SendQuery(*query, info, /* aUpdateTimer */ false));
+            }
+
+            if (nextTime > info.mRetransmissionTime)
+            {
+                nextTime = info.mRetransmissionTime;
+            }
 
 #if OPENTHREAD_CONFIG_DNS_CLIENT_OVER_TCP_ENABLE
-        if (info.mConfig.GetTransportProto() == QueryConfig::kDnsTransportTcp)
-        {
-            hasTcpQuery = true;
-        }
+            if (info.mConfig.GetTransportProto() == QueryConfig::kDnsTransportTcp)
+            {
+                hasTcpQuery = true;
+            }
 #endif
+        }
     }
 
     if (nextTime < now.GetDistantFuture())
@@ -1209,6 +1549,121 @@
 #endif
 }
 
+#if OPENTHREAD_CONFIG_DNS_CLIENT_NAT64_ENABLE
+
+Error Client::ReplaceWithIp4Query(Query &aQuery)
+{
+    Error     error = kErrorFailed;
+    QueryInfo info;
+
+    info.ReadFrom(aQuery);
+
+    VerifyOrExit(info.mQueryType == kIp4AddressQuery);
+    VerifyOrExit(info.mConfig.GetNat64Mode() == QueryConfig::kNat64Allow);
+
+    // We send a new query for IPv4 address resolution
+    // for the same host name. We reuse the existing `aQuery`
+    // instance and keep all the info but clear `mTransmissionCount`
+    // and `mMessageId` (so that a new random message ID is
+    // selected). The new `info` will be saved in the query in
+    // `SendQuery()`. Note that the current query is still in the
+    // `mMainQueries` list when `SendQuery()` selects a new random
+    // message ID, so the existing message ID for this query will
+    // not be reused.
+
+    info.mQueryType         = kIp4AddressQuery;
+    info.mMessageId         = 0;
+    info.mTransmissionCount = 0;
+
+    IgnoreError(SendQuery(aQuery, info, /* aUpdateTimer */ true));
+    error = kErrorNone;
+
+exit:
+    return error;
+}
+
+#endif // OPENTHREAD_CONFIG_DNS_CLIENT_NAT64_ENABLE
+
+#if OPENTHREAD_CONFIG_DNS_CLIENT_SERVICE_DISCOVERY_ENABLE
+
+Error Client::ReplaceWithSeparateSrvTxtQueries(Query &aQuery)
+{
+    Error     error = kErrorFailed;
+    QueryInfo info;
+    Query    *secondQuery;
+
+    info.ReadFrom(aQuery);
+
+    VerifyOrExit(info.mQueryType == kServiceQuerySrvTxt);
+    VerifyOrExit(info.mConfig.GetServiceMode() == QueryConfig::kServiceModeSrvTxtOptimize);
+
+    secondQuery = aQuery.Clone();
+    VerifyOrExit(secondQuery != nullptr);
+
+    info.mQueryType         = kServiceQueryTxt;
+    info.mMessageId         = 0;
+    info.mTransmissionCount = 0;
+    info.mMainQuery         = &aQuery;
+    IgnoreError(SendQuery(*secondQuery, info, /* aUpdateTimer */ true));
+
+    info.mQueryType         = kServiceQuerySrv;
+    info.mMessageId         = 0;
+    info.mTransmissionCount = 0;
+    info.mNextQuery         = secondQuery;
+    IgnoreError(SendQuery(aQuery, info, /* aUpdateTimer */ true));
+    error = kErrorNone;
+
+exit:
+    return error;
+}
+
+void Client::ResolveHostAddressIfNeeded(Query &aQuery, const Message &aResponseMessage)
+{
+    QueryInfo   info;
+    Response    response;
+    ServiceInfo serviceInfo;
+    char        hostName[Name::kMaxNameSize];
+
+    info.ReadFrom(aQuery);
+
+    VerifyOrExit(info.mQueryType == kServiceQuerySrvTxt || info.mQueryType == kServiceQuerySrv);
+    VerifyOrExit(info.mShouldResolveHostAddr);
+
+    PopulateResponse(response, aQuery, aResponseMessage);
+
+    memset(&serviceInfo, 0, sizeof(serviceInfo));
+    serviceInfo.mHostNameBuffer     = hostName;
+    serviceInfo.mHostNameBufferSize = sizeof(hostName);
+    SuccessOrExit(response.ReadServiceInfo(Response::kAnswerSection, Name(aQuery, kNameOffsetInQuery), serviceInfo));
+
+    // Check whether AAAA record for host address is provided in the SRV query response
+
+    if (AsCoreType(&serviceInfo.mHostAddress).IsUnspecified())
+    {
+        Query *newQuery;
+
+        info.mQueryType         = kIp6AddressQuery;
+        info.mMessageId         = 0;
+        info.mTransmissionCount = 0;
+        info.mMainQuery         = &FindMainQuery(aQuery);
+
+        SuccessOrExit(AllocateQuery(info, nullptr, hostName, newQuery));
+        IgnoreError(SendQuery(*newQuery, info, /* aUpdateTimer */ true));
+
+        // Update `aQuery` to be linked with new query (inserting
+        // the `newQuery` into the linked-list after `aQuery`).
+
+        info.ReadFrom(aQuery);
+        info.mNextQuery = newQuery;
+        UpdateQuery(aQuery, info);
+    }
+
+exit:
+    return;
+}
+
+#endif // OPENTHREAD_CONFIG_DNS_CLIENT_SERVICE_DISCOVERY_ENABLE
+
 #if OPENTHREAD_CONFIG_DNS_CLIENT_OVER_TCP_ENABLE
 void Client::PrepareTcpMessage(Message &aMessage)
 {
@@ -1257,7 +1712,7 @@
     // and copy the content into `aMessage`. As we read we can move
     // to the next `aLinkedBuffer` and update `aOffset`.
     // Returns:
-    // - `kErrorNone` if `aLengh` bytes are successfully read and
+    // - `kErrorNone` if `aLength` bytes are successfully read and
     //    `aOffset` and `aLinkedBuffer` are updated.
     // - `kErrorNotFound` is not enough bytes available to read
     //    from `aLinkedBuffer`.
@@ -1314,7 +1769,7 @@
     SuccessOrExit(mEndpoint.ReceiveByReference(data));
     VerifyOrExit(data != nullptr);
 
-    message = mSocket.NewMessage(0);
+    message = mSocket.NewMessage();
     VerifyOrExit(message != nullptr);
 
     while (aBytesAvailable > totalRead)
@@ -1334,24 +1789,7 @@
         totalRead += length + sizeof(uint16_t);
 
         // Now process the read message as query response.
-        {
-            Response  response;
-            QueryType type;
-            Error     responseError;
-
-            response.mInstance = &Get<Instance>();
-            response.mMessage  = message;
-
-            if (ParseResponse(response, type, responseError) == kErrorNone)
-            {
-                if (responseError == kErrorNone && length > OPENTHREAD_CONFIG_DNS_CLIENT_OVER_TCP_QUERY_MAX_SIZE)
-                {
-                    LogWarn("Dns query over TCP wasn't received - message is too big.");
-                    responseError = kErrorNoBufs;
-                }
-                FinalizeQuery(response, type, responseError);
-            }
-        }
+        ProcessResponse(*message);
 
         IgnoreError(message->SetLength(0));
 
@@ -1383,12 +1821,13 @@
     mTcpState = kTcpUninitialized;
 
     // Abort queries in case of connection failures
-    for (Query &query : mQueries)
+    for (Query &mainQuery : mMainQueries)
     {
-        info.ReadFrom(query);
+        info.ReadFrom(mainQuery);
+
         if (info.mConfig.GetTransportProto() == QueryConfig::kDnsTransportTcp)
         {
-            FinalizeQuery(query, kErrorAbort);
+            FinalizeQuery(mainQuery, kErrorAbort);
         }
     }
 }
diff --git a/src/core/net/dns_client.hpp b/src/core/net/dns_client.hpp
index e314745..d8b7f13 100644
--- a/src/core/net/dns_client.hpp
+++ b/src/core/net/dns_client.hpp
@@ -146,6 +146,20 @@
 #endif
 
         /**
+         * This enumeration type represents the service resolution mode.
+         *
+         */
+        enum ServiceMode : uint8_t
+        {
+            kServiceModeUnspecified    = OT_DNS_SERVICE_MODE_UNSPECIFIED,      ///< Unspecified. Use default.
+            kServiceModeSrv            = OT_DNS_SERVICE_MODE_SRV,              ///< SRV record only.
+            kServiceModeTxt            = OT_DNS_SERVICE_MODE_TXT,              ///< TXT record only.
+            kServiceModeSrvTxt         = OT_DNS_SERVICE_MODE_SRV_TXT,          ///< SRV and TXT same msg.
+            kServiceModeSrvTxtSeparate = OT_DNS_SERVICE_MODE_SRV_TXT_SEPARATE, ///< SRV and TXT separate msgs.
+            kServiceModeSrvTxtOptimize = OT_DNS_SERVICE_MODE_SRV_TXT_OPTIMIZE, ///< Same msg first, if fail separate.
+        };
+
+        /**
          * This enumeration type represents the DNS transport protocol selection.
          *
          */
@@ -206,6 +220,13 @@
          */
         Nat64Mode GetNat64Mode(void) const { return static_cast<Nat64Mode>(mNat64Mode); }
 #endif
+        /**
+         * This method gets the service resolution mode.
+         *
+         * @returns The service resolution mode.
+         *
+         */
+        ServiceMode GetServiceMode(void) const { return static_cast<ServiceMode>(mServiceMode); }
 
         /**
          * This method gets the transport protocol.
@@ -220,6 +241,10 @@
         static constexpr uint16_t kDefaultServerPort      = OPENTHREAD_CONFIG_DNS_CLIENT_DEFAULT_SERVER_PORT;
         static constexpr uint8_t  kDefaultMaxTxAttempts   = OPENTHREAD_CONFIG_DNS_CLIENT_DEFAULT_MAX_TX_ATTEMPTS;
         static constexpr bool kDefaultRecursionDesired    = OPENTHREAD_CONFIG_DNS_CLIENT_DEFAULT_RECURSION_DESIRED_FLAG;
+        static constexpr ServiceMode kDefaultServiceMode =
+            static_cast<ServiceMode>(OPENTHREAD_CONFIG_DNS_CLIENT_DEFAULT_SERVICE_MODE);
+
+        static_assert(kDefaultServiceMode != kServiceModeUnspecified, "Invalid default service mode");
 
 #if OPENTHREAD_CONFIG_DNS_CLIENT_NAT64_ENABLE
         static constexpr bool kDefaultNat64Allowed = OPENTHREAD_CONFIG_DNS_CLIENT_DEFAULT_NAT64_ALLOWED;
@@ -239,6 +264,7 @@
         void SetResponseTimeout(uint32_t aResponseTimeout) { mResponseTimeout = aResponseTimeout; }
         void SetMaxTxAttempts(uint8_t aMaxTxAttempts) { mMaxTxAttempts = aMaxTxAttempts; }
         void SetRecursionFlag(RecursionFlag aFlag) { mRecursionFlag = static_cast<otDnsRecursionFlag>(aFlag); }
+        void SetServiceMode(ServiceMode aMode) { mServiceMode = static_cast<otDnsServiceMode>(aMode); }
 #if OPENTHREAD_CONFIG_DNS_CLIENT_NAT64_ENABLE
         void SetNat64Mode(Nat64Mode aMode) { mNat64Mode = static_cast<otDnsNat64Mode>(aMode); }
 #endif
@@ -246,7 +272,8 @@
         {
             mTransportProto = static_cast<otDnsTransportProto>(aTransportProto);
         }
-        void SetFrom(const QueryConfig &aConfig, const QueryConfig &aDefaultConfig);
+
+        void SetFrom(const QueryConfig *aConfig, const QueryConfig &aDefaultConfig);
     };
 
 #if OPENTHREAD_CONFIG_DNS_CLIENT_SERVICE_DISCOVERY_ENABLE
@@ -292,12 +319,16 @@
 #endif
 
 #if OPENTHREAD_CONFIG_DNS_CLIENT_SERVICE_DISCOVERY_ENABLE
-        Error FindServiceInfo(Section aSection, const Name &aName, ServiceInfo &aServiceInfo) const;
+        void  InitServiceInfo(ServiceInfo &aServiceInfo) const;
+        Error ReadServiceInfo(Section aSection, const Name &aName, ServiceInfo &aServiceInfo) const;
+        Error ReadTxtRecord(Section aSection, const Name &aName, ServiceInfo &aServiceInfo) const;
 #endif
+        void PopulateFrom(const Message &aMessage);
 
         Instance      *mInstance;              // The OpenThread instance.
         Query         *mQuery;                 // The associated query.
         const Message *mMessage;               // The response message.
+        Response      *mNext;                  // The next response when we have related queries.
         uint16_t       mAnswerOffset;          // Answer section offset in `mMessage`.
         uint16_t       mAnswerRecordCount;     // Number of records in answer section.
         uint16_t       mAdditionalOffset;      // Additional data section offset in `mMessage`.
@@ -687,7 +718,7 @@
                  const QueryConfig *aConfig = nullptr);
 
     /**
-     * This method sends a DNS service instance resolution query for a given service instance.
+     * This method starts a DNS service instance resolution for a given service instance.
      *
      * The @p aConfig can be `nullptr`. In this case the default config (from `GetDefaultConfig()`) will be used as
      * the config for this query. In a non-`nullptr` @p aConfig, some of the fields can be left unspecified (value
@@ -710,6 +741,32 @@
                          void                *aContext,
                          const QueryConfig   *aConfig = nullptr);
 
+    /**
+     * This method starts a DNS service instance resolution for a given service instance, with a potential follow-up
+     * host name resolution (if the server/resolver does not provide AAAA/A records for the host name in the response
+     * to SRV query).
+     *
+     * The @p aConfig can be `nullptr`. In this case the default config (from `GetDefaultConfig()`) will be used as
+     * the config for this query. In a non-`nullptr` @p aConfig, some of the fields can be left unspecified (value
+     * zero). The unspecified fields are then replaced by the values from the default config.
+     *
+     * @param[in]  aInstanceLabel     The service instance label.
+     * @param[in]  aServiceName       The service name (together with @p aInstanceLabel form full instance name).
+     * @param[in]  aCallback          A function pointer that shall be called on response reception or time-out.
+     * @param[in]  aContext           A pointer to arbitrary context information.
+     * @param[in]  aConfig            The config to use for this query.
+     *
+     * @retval kErrorNone         Query sent successfully. @p aCallback will be invoked to report the status.
+     * @retval kErrorNoBufs       Insufficient buffer to prepare and send query.
+     * @retval kErrorInvalidArgs  @p aInstanceLabel is `nullptr` or the @p aConfig is invalid.
+     *
+     */
+    Error ResolveServiceAndHostAddress(const char        *aInstanceLabel,
+                                       const char        *aServiceName,
+                                       ServiceCallback    aCallback,
+                                       void              *aContext,
+                                       const QueryConfig *aConfig = nullptr);
+
 #endif // OPENTHREAD_CONFIG_DNS_CLIENT_SERVICE_DISCOVERY_ENABLE
 
 private:
@@ -720,16 +777,19 @@
         kIp4AddressQuery, // IPv4 Address resolution
 #endif
 #if OPENTHREAD_CONFIG_DNS_CLIENT_SERVICE_DISCOVERY_ENABLE
-        kBrowseQuery,  // Browse (service instance enumeration).
-        kServiceQuery, // Service instance resolution.
+        kBrowseQuery,        // Browse (service instance enumeration).
+        kServiceQuerySrvTxt, // Service instance resolution both SRV and TXT records.
+        kServiceQuerySrv,    // Service instance resolution SRV record only.
+        kServiceQueryTxt,    // Service instance resolution TXT record only.
 #endif
+        kNoQuery,
     };
 
 #if OPENTHREAD_CONFIG_DNS_CLIENT_OVER_TCP_ENABLE
     enum TcpState : uint8_t
     {
         kTcpUninitialized = 0,
-        kTcpConecting,
+        kTcpConnecting,
         kTcpConnectedIdle,
         kTcpConnectedSending,
     };
@@ -757,30 +817,53 @@
         TimeMilli   mRetransmissionTime;
         QueryConfig mConfig;
         uint8_t     mTransmissionCount;
+        bool        mShouldResolveHostAddr;
+        Query      *mMainQuery;
+        Query      *mNextQuery;
+        Message    *mSavedResponse;
         // Followed by the name (service, host, instance) encoded as a `Dns::Name`.
     };
 
     static constexpr uint16_t kNameOffsetInQuery = sizeof(QueryInfo);
 
-    Error       StartQuery(QueryInfo         &aInfo,
-                           const QueryConfig *aConfig,
-                           const char        *aLabel,
-                           const char        *aName,
-                           void              *aContext);
+    Error       StartQuery(QueryInfo &aInfo, const char *aLabel, const char *aName, QueryType aSecondType = kNoQuery);
     Error       AllocateQuery(const QueryInfo &aInfo, const char *aLabel, const char *aName, Query *&aQuery);
     void        FreeQuery(Query &aQuery);
     void        UpdateQuery(Query &aQuery, const QueryInfo &aInfo) { aQuery.Write(0, aInfo); }
+    Query      &FindMainQuery(Query &aQuery);
     Error       SendQuery(Query &aQuery, QueryInfo &aInfo, bool aUpdateTimer);
     void        FinalizeQuery(Query &aQuery, Error aError);
-    void        FinalizeQuery(Response &Response, QueryType aType, Error aError);
-    static void GetCallback(const Query &aQuery, Callback &aCallback, void *&aContext);
+    void        FinalizeQuery(Response &Response, Error aError);
+    static void GetQueryTypeAndCallback(const Query &aQuery, QueryType &aType, Callback &aCallback, void *&aContext);
     Error       AppendNameFromQuery(const Query &aQuery, Message &aMessage);
     Query      *FindQueryById(uint16_t aMessageId);
     static void HandleUdpReceive(void *aContext, otMessage *aMessage, const otMessageInfo *aMsgInfo);
-    void        ProcessResponse(const Message &aMessage);
-    Error       ParseResponse(Response &aResponse, QueryType &aType, Error &aResponseError);
+    void        ProcessResponse(const Message &aResponseMessage);
+    Error       ParseResponse(const Message &aResponseMessage, Query *&aQuery, Error &aResponseError);
+    bool        CanFinalizeQuery(Query &aQuery);
+    void        SaveQueryResponse(Query &aQuery, const Message &aResponseMessage);
+    Query      *PopulateResponse(Response &aResponse, Query &aQuery, const Message &aResponseMessage);
+    void        PrepareResponseAndFinalize(Query &aQuery, const Message &aResponseMessage, Response *aPrevResponse);
     void        HandleTimer(void);
 
+#if OPENTHREAD_CONFIG_DNS_CLIENT_NAT64_ENABLE
+    Error ReplaceWithIp4Query(Query &aQuery);
+#endif
+#if OPENTHREAD_CONFIG_DNS_CLIENT_SERVICE_DISCOVERY_ENABLE
+    Error Resolve(const char        *aInstanceLabel,
+                  const char        *aServiceName,
+                  ServiceCallback    aCallback,
+                  void              *aContext,
+                  const QueryConfig *aConfig,
+                  bool               aShouldResolveHostAddr);
+    Error ReplaceWithSeparateSrvTxtQueries(Query &aQuery);
+    void  ResolveHostAddressIfNeeded(Query &aQuery, const Message &aResponseMessage);
+#endif
+
+#if OPENTHREAD_CONFIG_DNS_CLIENT_DEFAULT_SERVER_ADDRESS_AUTO_SET_ENABLE
+    void UpdateDefaultConfigAddress(void);
+#endif
+
 #if OPENTHREAD_CONFIG_DNS_CLIENT_OVER_TCP_ENABLE
     static void HandleTcpEstablishedCallback(otTcpEndpoint *aEndpoint);
     static void HandleTcpSendDoneCallback(otTcpEndpoint *aEndpoint, otLinkedBuffer *aData);
@@ -805,15 +888,8 @@
     void  PrepareTcpMessage(Message &aMessage);
 #endif // OPENTHREAD_CONFIG_DNS_CLIENT_OVER_TCP_ENABLE
 
-#if OPENTHREAD_CONFIG_DNS_CLIENT_NAT64_ENABLE
-    Error CheckAddressResponse(Response &aResponse, Error aResponseError) const;
-#endif
-#if OPENTHREAD_CONFIG_DNS_CLIENT_DEFAULT_SERVER_ADDRESS_AUTO_SET_ENABLE
-    void UpdateDefaultConfigAddress(void);
-#endif
-
-    static const uint8_t   kQuestionCount[];
-    static const uint16_t *kQuestionRecordTypes[];
+    static const uint8_t         kQuestionCount[];
+    static const uint16_t *const kQuestionRecordTypes[];
 
     static const uint16_t kIp6AddressQueryRecordTypes[];
 #if OPENTHREAD_CONFIG_DNS_CLIENT_NAT64_ENABLE
@@ -840,7 +916,7 @@
     TcpState mTcpState;
 #endif
 
-    QueryList   mQueries;
+    QueryList   mMainQueries;
     RetryTimer  mTimer;
     QueryConfig mDefaultConfig;
 #if OPENTHREAD_CONFIG_DNS_CLIENT_DEFAULT_SERVER_ADDRESS_AUTO_SET_ENABLE
diff --git a/src/core/net/dns_types.cpp b/src/core/net/dns_types.cpp
index a5a964a..7c3241e 100644
--- a/src/core/net/dns_types.cpp
+++ b/src/core/net/dns_types.cpp
@@ -183,7 +183,7 @@
     {
         ch = index < aLength ? aLabels[index] : static_cast<char>(kNullChar);
 
-        if ((ch == kNullChar) || (ch == kLabelSeperatorChar))
+        if ((ch == kNullChar) || (ch == kLabelSeparatorChar))
         {
             uint8_t labelLength = static_cast<uint8_t>(index - labelStartIndex);
 
@@ -328,7 +328,7 @@
 
             if (!firstLabel)
             {
-                *aNameBuffer++ = kLabelSeperatorChar;
+                *aNameBuffer++ = kLabelSeparatorChar;
                 aNameBufferSize--;
 
                 // No need to check if we have reached end of the name buffer
@@ -345,7 +345,7 @@
         case kErrorNotFound:
             // We reach the end of name successfully. Always add a terminating dot
             // at the end.
-            *aNameBuffer++ = kLabelSeperatorChar;
+            *aNameBuffer++ = kLabelSeparatorChar;
             aNameBufferSize--;
             VerifyOrExit(aNameBufferSize >= sizeof(uint8_t), error = kErrorNoBufs);
             *aNameBuffer = kNullChar;
@@ -382,7 +382,7 @@
     LabelIterator iterator(aMessage, aOffset);
     bool          matches = true;
 
-    if (*aName == kLabelSeperatorChar)
+    if (*aName == kLabelSeparatorChar)
     {
         aName++;
         VerifyOrExit(*aName == kNullChar, error = kErrorInvalidArgs);
@@ -561,7 +561,7 @@
 
     if (!aAllowDotCharInLabel)
     {
-        VerifyOrExit(StringFind(aLabelBuffer, kLabelSeperatorChar) == nullptr, error = kErrorParse);
+        VerifyOrExit(StringFind(aLabelBuffer, kLabelSeparatorChar) == nullptr, error = kErrorParse);
     }
 
 exit:
@@ -598,7 +598,7 @@
 
     matches = (*aName == kNullChar);
 
-    if (!aIsSingleLabel && (*aName == kLabelSeperatorChar))
+    if (!aIsSingleLabel && (*aName == kLabelSeparatorChar))
     {
         matches = true;
         aName++;
@@ -641,13 +641,13 @@
     uint16_t nameLength        = StringLength(aName, kMaxNameLength);
     uint16_t domainLength      = StringLength(aDomain, kMaxNameLength);
 
-    if (nameLength > 0 && aName[nameLength - 1] == kLabelSeperatorChar)
+    if (nameLength > 0 && aName[nameLength - 1] == kLabelSeparatorChar)
     {
         nameEndsWithDot = true;
         --nameLength;
     }
 
-    if (domainLength > 0 && aDomain[domainLength - 1] == kLabelSeperatorChar)
+    if (domainLength > 0 && aDomain[domainLength - 1] == kLabelSeparatorChar)
     {
         domainEndsWithDot = true;
         --domainLength;
@@ -659,7 +659,7 @@
 
     if (nameLength > domainLength)
     {
-        VerifyOrExit(aName[-1] == kLabelSeperatorChar);
+        VerifyOrExit(aName[-1] == kLabelSeparatorChar);
     }
 
     // This method allows either `aName` or `aDomain` to include or
diff --git a/src/core/net/dns_types.hpp b/src/core/net/dns_types.hpp
index 7ee4005..19b9061 100644
--- a/src/core/net/dns_types.hpp
+++ b/src/core/net/dns_types.hpp
@@ -514,7 +514,7 @@
      */
     static constexpr uint8_t kMaxLabelLength = kMaxLabelSize - 1;
 
-    static constexpr char kLabelSeperatorChar = '.';
+    static constexpr char kLabelSeparatorChar = '.';
 
     /**
      * This enumeration represents the name type.
@@ -1020,7 +1020,7 @@
     static constexpr uint16_t kPointerLabelTypeUint16 = 0xc000; // Pointer label type mask (first 2 bits).
     static constexpr uint16_t kPointerLabelOffsetMask = 0x3fff; // Mask for offset in a pointer label (lower 14 bits).
 
-    static constexpr bool kIsSingleLabel = true; // Used in `LabelIterator::CompareLable()`.
+    static constexpr bool kIsSingleLabel = true; // Used in `LabelIterator::CompareLabel()`.
 
     struct LabelIterator
     {
@@ -2361,7 +2361,7 @@
      * @param[in] aExtendedResponse The upper 8-bit of the extended 12-bit Response Code.
      *
      */
-    void SetExtnededResponseCode(uint8_t aExtendedResponse) { GetTtlByteAt(kExtRCodeByteIndex) = aExtendedResponse; }
+    void SetExtendedResponseCode(uint8_t aExtendedResponse) { GetTtlByteAt(kExtRCodeByteIndex) = aExtendedResponse; }
 
     /**
      * This method gets the Version field.
diff --git a/src/core/net/dnssd_server.cpp b/src/core/net/dnssd_server.cpp
index f2c4152..478e4d8 100644
--- a/src/core/net/dnssd_server.cpp
+++ b/src/core/net/dnssd_server.cpp
@@ -70,6 +70,7 @@
     , mEnableUpstreamQuery(false)
 #endif
     , mTimer(aInstance)
+    , mTestMode(kTestModeDisabled)
 {
     mCounters.Clear();
 }
@@ -178,7 +179,7 @@
     }
 #endif
 
-    responseMessage = mSocket.NewMessage(0);
+    responseMessage = mSocket.NewMessage();
     VerifyOrExit(responseMessage != nullptr, error = kErrorNoBufs);
 
     // Allocate space for DNS header
@@ -203,6 +204,11 @@
     VerifyOrExit(!aRequestHeader.IsTruncationFlagSet(), response = Header::kResponseFormatError);
     VerifyOrExit(aRequestHeader.GetQuestionCount() > 0, response = Header::kResponseFormatError);
 
+    if (mTestMode & kTestModeSingleQuestionOnly)
+    {
+        VerifyOrExit(aRequestHeader.GetQuestionCount() == 1, response = Header::kResponseFormatError);
+    }
+
     response = AddQuestions(aRequestHeader, aRequestMessage, responseHeader, *responseMessage, compressInfo);
     VerifyOrExit(response == Header::kResponseSuccess);
 
@@ -676,10 +682,10 @@
     uint8_t end;
 
     VerifyOrExit(start > 0, error = kErrorNotFound);
-    VerifyOrExit(aName[--start] == Name::kLabelSeperatorChar, error = kErrorInvalidArgs);
+    VerifyOrExit(aName[--start] == Name::kLabelSeparatorChar, error = kErrorInvalidArgs);
 
     end = start;
-    while (start > 0 && aName[start - 1] != Name::kLabelSeperatorChar)
+    while (start > 0 && aName[start - 1] != Name::kLabelSeparatorChar)
     {
         start--;
     }
@@ -719,6 +725,8 @@
     // Answer the questions with additional RRs if required
     if (aResponseHeader.GetAnswerCount() > 0)
     {
+        VerifyOrExit(!(mTestMode & kTestModeEmptyAdditionalSection));
+
         readOffset = sizeof(Header);
         for (uint16_t i = 0; i < aResponseHeader.GetQuestionCount(); i++)
         {
@@ -1036,6 +1044,11 @@
 
     for (uint8_t additional = 0; additional <= 1; additional++)
     {
+        if (additional == 1)
+        {
+            VerifyOrExit(!(mTestMode & kTestModeEmptyAdditionalSection));
+        }
+
         if (HasQuestion(aQuery.GetResponseHeader(), aQuery.GetResponseMessage(), aInstanceInfo.mFullName,
                         ResourceRecord::kTypeSrv) == !additional)
         {
@@ -1116,9 +1129,9 @@
 void Server::HandleDiscoveredServiceInstance(const char                       *aServiceFullName,
                                              const otDnssdServiceInstanceInfo &aInstanceInfo)
 {
-    OT_ASSERT(StringEndsWith(aServiceFullName, Name::kLabelSeperatorChar));
-    OT_ASSERT(StringEndsWith(aInstanceInfo.mFullName, Name::kLabelSeperatorChar));
-    OT_ASSERT(StringEndsWith(aInstanceInfo.mHostName, Name::kLabelSeperatorChar));
+    OT_ASSERT(StringEndsWith(aServiceFullName, Name::kLabelSeparatorChar));
+    OT_ASSERT(StringEndsWith(aInstanceInfo.mFullName, Name::kLabelSeparatorChar));
+    OT_ASSERT(StringEndsWith(aInstanceInfo.mHostName, Name::kLabelSeparatorChar));
 
     for (QueryTransaction &query : mQueryTransactions)
     {
@@ -1131,7 +1144,7 @@
 
 void Server::HandleDiscoveredHost(const char *aHostFullName, const otDnssdHostInfo &aHostInfo)
 {
-    OT_ASSERT(StringEndsWith(aHostFullName, Name::kLabelSeperatorChar));
+    OT_ASSERT(StringEndsWith(aHostFullName, Name::kLabelSeparatorChar));
 
     for (QueryTransaction &query : mQueryTransactions)
     {
diff --git a/src/core/net/dnssd_server.hpp b/src/core/net/dnssd_server.hpp
index cf38ffc..cf708f5 100644
--- a/src/core/net/dnssd_server.hpp
+++ b/src/core/net/dnssd_server.hpp
@@ -262,6 +262,29 @@
      */
     const Counters &GetCounters(void) const { return mCounters; };
 
+    /**
+     * This enumeration represents different test mode flags for use in `SetTestMode()`.
+     *
+     */
+    enum TestModeFlags : uint8_t
+    {
+        kTestModeSingleQuestionOnly     = 1 << 0, ///< Allow single question in query, send `FormatError` otherwise.
+        kTestModeEmptyAdditionalSection = 1 << 1, ///< Do not include any RR in additional section.
+    };
+
+    static constexpr uint8_t kTestModeDisabled = 0; ///< Test mode is disabled (no flags).
+
+    /**
+     * This method sets the test mode for `Server`.
+     *
+     * The test mode flags are intended for testing the client by having server behave in certain ways, e.g., reject
+     * messages with certain format (e.g., more than one question in query).
+     *
+     * @param[in] aTestMode   The new test mode (combination of `TestModeFlags`).
+     *
+     */
+    void SetTestMode(uint8_t aTestMode) { mTestMode = aTestMode; }
+
 private:
     class NameCompressInfo : public Clearable<NameCompressInfo>
     {
@@ -531,8 +554,8 @@
 #endif
 
     ServerTimer mTimer;
-
-    Counters mCounters;
+    Counters    mCounters;
+    uint8_t     mTestMode;
 };
 
 } // namespace ServiceDiscovery
diff --git a/src/core/net/icmp6.cpp b/src/core/net/icmp6.cpp
index 5b5e648..2f195b0 100644
--- a/src/core/net/icmp6.cpp
+++ b/src/core/net/icmp6.cpp
@@ -54,7 +54,7 @@
 {
 }
 
-Message *Icmp::NewMessage(uint16_t aReserved) { return Get<Ip6>().NewMessage(sizeof(Header) + aReserved); }
+Message *Icmp::NewMessage(void) { return Get<Ip6>().NewMessage(sizeof(Header)); }
 
 Error Icmp::RegisterHandler(Handler &aHandler) { return mHandlers.Add(aHandler); }
 
diff --git a/src/core/net/icmp6.hpp b/src/core/net/icmp6.hpp
index 2d073c5..b054b56 100644
--- a/src/core/net/icmp6.hpp
+++ b/src/core/net/icmp6.hpp
@@ -242,12 +242,10 @@
     /**
      * This method returns a new ICMP message with sufficient header space reserved.
      *
-     * @param[in]  aReserved  The number of header bytes to reserve after the ICMP header.
-     *
      * @returns A pointer to the message or `nullptr` if no buffers are available.
      *
      */
-    Message *NewMessage(uint16_t aReserved);
+    Message *NewMessage(void);
 
     /**
      * This method registers ICMPv6 handler.
diff --git a/src/core/net/ip4_types.cpp b/src/core/net/ip4_types.cpp
index 449d108..bbe80f1 100644
--- a/src/core/net/ip4_types.cpp
+++ b/src/core/net/ip4_types.cpp
@@ -37,44 +37,27 @@
 namespace ot {
 namespace Ip4 {
 
-Error Address::FromString(const char *aString)
+Error Address::FromString(const char *aString, char aTerminatorChar)
 {
-    constexpr char kSeperatorChar = '.';
-    constexpr char kNullChar      = '\0';
+    constexpr char kSeparatorChar = '.';
 
-    Error error = kErrorParse;
+    Error       error = kErrorParse;
+    const char *cur   = aString;
 
     for (uint8_t index = 0;; index++)
     {
-        uint16_t value         = 0;
-        uint8_t  hasFirstDigit = false;
-
-        for (char digitChar = *aString;; ++aString, digitChar = *aString)
-        {
-            if ((digitChar < '0') || (digitChar > '9'))
-            {
-                break;
-            }
-
-            value = static_cast<uint16_t>((value * 10) + static_cast<uint8_t>(digitChar - '0'));
-            VerifyOrExit(value <= NumericLimits<uint8_t>::kMax);
-            hasFirstDigit = true;
-        }
-
-        VerifyOrExit(hasFirstDigit);
-
-        mFields.m8[index] = static_cast<uint8_t>(value);
+        SuccessOrExit(StringParseUint8(cur, mFields.m8[index]));
 
         if (index == sizeof(Address) - 1)
         {
             break;
         }
 
-        VerifyOrExit(*aString == kSeperatorChar);
-        aString++;
+        VerifyOrExit(*cur == kSeparatorChar);
+        cur++;
     }
 
-    VerifyOrExit(*aString == kNullChar);
+    VerifyOrExit(*cur == aTerminatorChar);
     error = kErrorNone;
 
 exit:
@@ -148,6 +131,29 @@
     return string;
 }
 
+Error Cidr::FromString(const char *aString)
+{
+    constexpr char     kSlashChar     = '/';
+    constexpr uint16_t kMaxCidrLength = 32;
+
+    Error       error = kErrorParse;
+    const char *cur;
+
+    SuccessOrExit(AsCoreType(&mAddress).FromString(aString, kSlashChar));
+
+    cur = StringFind(aString, kSlashChar);
+    VerifyOrExit(cur != nullptr);
+    cur++;
+
+    SuccessOrExit(StringParseUint8(cur, mLength, kMaxCidrLength));
+    VerifyOrExit(*cur == kNullChar);
+
+    error = kErrorNone;
+
+exit:
+    return error;
+}
+
 void Cidr::ToString(StringWriter &aWriter) const
 {
     aWriter.Append("%s/%d", AsCoreType(&mAddress).ToString().AsCString(), mLength);
diff --git a/src/core/net/ip4_types.hpp b/src/core/net/ip4_types.hpp
index fa4bb41..6d2ba0c 100644
--- a/src/core/net/ip4_types.hpp
+++ b/src/core/net/ip4_types.hpp
@@ -148,7 +148,7 @@
     void SynthesizeFromCidrAndHost(const Cidr &aCidr, uint32_t aHost);
 
     /**
-     * This method parses an IPv4 address string.
+     * This method parses an IPv4 address string terminated by `aTerminatorChar`.
      *
      * The string MUST follow the quad-dotted notation of four decimal values (ranging from 0 to 255 each). For
      * example, "127.0.0.1"
@@ -159,7 +159,7 @@
      * @retval kErrorParse        Failed to parse the IPv4 address string.
      *
      */
-    Error FromString(const char *aString);
+    Error FromString(const char *aString, char aTerminatorChar = kNullChar);
 
     /**
      * This method converts the address to a string.
@@ -207,6 +207,20 @@
     typedef String<Address::kAddressStringSize + kCidrSuffixSize> InfoString;
 
     /**
+     * This method converts the IPv4 CIDR string to binary.
+     *
+     * The string format uses quad-dotted notation of four bytes in the address with the length of prefix (e.g.,
+     * "127.0.0.1/32").
+     *
+     * @param[in]  aString  A pointer to the null-terminated string.
+     *
+     * @retval kErrorNone          Successfully parsed the IPv4 CIDR string.
+     * @retval kErrorParse         Failed to parse the IPv4 CIDR string.
+     *
+     */
+    Error FromString(const char *aString);
+
+    /**
      * This method converts the IPv4 CIDR to a string.
      *
      * The string format uses quad-dotted notation of four bytes in the address with the length of prefix (e.g.,
@@ -284,7 +298,7 @@
     static constexpr uint8_t kVersionIhlOffset         = 0;
     static constexpr uint8_t kTrafficClassOffset       = 1;
     static constexpr uint8_t kTotalLengthOffset        = 2;
-    static constexpr uint8_t kIdenficationOffset       = 4;
+    static constexpr uint8_t kIdentificationOffset     = 4;
     static constexpr uint8_t kFlagsFragmentOffset      = 6;
     static constexpr uint8_t kTtlOffset                = 8;
     static constexpr uint8_t kProtocolOffset           = 9;
@@ -513,7 +527,7 @@
      * @returns Whether don't fragment flag is set.
      *
      */
-    bool GetDf(void) const { return HostSwap16(mFlagsFargmentOffset) & kFlagsDf; }
+    bool GetDf(void) const { return HostSwap16(mFlagsFragmentOffset) & kFlagsDf; }
 
     /**
      * This method returns the Mf flag in the IPv4 header.
@@ -521,7 +535,7 @@
      * @returns Whether more fragments flag is set.
      *
      */
-    bool GetMf(void) const { return HostSwap16(mFlagsFargmentOffset) & kFlagsMf; }
+    bool GetMf(void) const { return HostSwap16(mFlagsFragmentOffset) & kFlagsMf; }
 
     /**
      * This method returns the fragment offset in the IPv4 header.
@@ -529,7 +543,7 @@
      * @returns The fragment offset of the IPv4 packet.
      *
      */
-    uint16_t GetFragmentOffset(void) const { return HostSwap16(mFlagsFargmentOffset) & kFragmentOffsetMask; }
+    uint16_t GetFragmentOffset(void) const { return HostSwap16(mFlagsFragmentOffset) & kFragmentOffsetMask; }
 
 private:
     // IPv4 header
@@ -561,7 +575,7 @@
     uint8_t  mDscpEcn;
     uint16_t mTotalLength;
     uint16_t mIdentification;
-    uint16_t mFlagsFargmentOffset;
+    uint16_t mFlagsFragmentOffset;
     uint8_t  mTtl;
     uint8_t  mProtocol;
     uint16_t mHeaderChecksum;
@@ -571,7 +585,7 @@
 
 /**
  * This class implements ICMP(v4).
- * Note: ICMP(v4) messages will only be generated / handled by NAT64. So only header defination is required.
+ * Note: ICMP(v4) messages will only be generated / handled by NAT64. So only header definition is required.
  *
  */
 class Icmp
@@ -670,9 +684,9 @@
          * @param[in] aRestOfHeader The rest of header field in the ICMP message. The buffer should have 4 octets.
          *
          */
-        void SetRestOfHeader(const uint8_t *aRestOfheader)
+        void SetRestOfHeader(const uint8_t *aRestOfHeader)
         {
-            memcpy(mRestOfHeader, aRestOfheader, sizeof(mRestOfHeader));
+            memcpy(mRestOfHeader, aRestOfHeader, sizeof(mRestOfHeader));
         }
 
     private:
diff --git a/src/core/net/ip6.cpp b/src/core/net/ip6.cpp
index 2ba601b..b6693d2 100644
--- a/src/core/net/ip6.cpp
+++ b/src/core/net/ip6.cpp
@@ -81,15 +81,31 @@
 #endif
 }
 
+Message *Ip6::NewMessage(void) { return NewMessage(0); }
+
+Message *Ip6::NewMessage(uint16_t aReserved) { return NewMessage(aReserved, Message::Settings::GetDefault()); }
+
 Message *Ip6::NewMessage(uint16_t aReserved, const Message::Settings &aSettings)
 {
     return Get<MessagePool>().Allocate(
         Message::kTypeIp6, sizeof(Header) + sizeof(HopByHopHeader) + sizeof(MplOption) + aReserved, aSettings);
 }
 
-Message *Ip6::NewMessage(const uint8_t *aData, uint16_t aDataLength, const Message::Settings &aSettings)
+Message *Ip6::NewMessageFromData(const uint8_t *aData, uint16_t aDataLength, const Message::Settings &aSettings)
 {
-    Message *message = Get<MessagePool>().Allocate(Message::kTypeIp6, /* aReserveHeader */ 0, aSettings);
+    Message          *message  = nullptr;
+    Message::Settings settings = aSettings;
+    const Header     *header;
+
+    VerifyOrExit((aData != nullptr) && (aDataLength >= sizeof(Header)));
+
+    // Determine priority from IPv6 header
+    header = reinterpret_cast<const Header *>(aData);
+    VerifyOrExit(header->IsValid());
+    VerifyOrExit(sizeof(Header) + header->GetPayloadLength() == aDataLength);
+    settings.mPriority = DscpToPriority(header->GetDscp());
+
+    message = Get<MessagePool>().Allocate(Message::kTypeIp6, /* aReserveHeader */ 0, settings);
 
     VerifyOrExit(message != nullptr);
 
@@ -103,18 +119,6 @@
     return message;
 }
 
-Message *Ip6::NewMessage(const uint8_t *aData, uint16_t aDataLength)
-{
-    Message          *message = nullptr;
-    Message::Priority priority;
-
-    SuccessOrExit(GetDatagramPriority(aData, aDataLength, priority));
-    message = NewMessage(aData, aDataLength, Message::Settings(Message::kWithLinkSecurity, priority));
-
-exit:
-    return message;
-}
-
 Message::Priority Ip6::DscpToPriority(uint8_t aDscp)
 {
     Message::Priority priority;
@@ -170,23 +174,6 @@
     return dscp;
 }
 
-Error Ip6::GetDatagramPriority(const uint8_t *aData, uint16_t aDataLen, Message::Priority &aPriority)
-{
-    Error         error = kErrorNone;
-    const Header *header;
-
-    VerifyOrExit((aData != nullptr) && (aDataLen >= sizeof(Header)), error = kErrorInvalidArgs);
-
-    header = reinterpret_cast<const Header *>(aData);
-    VerifyOrExit(header->IsValid(), error = kErrorParse);
-    VerifyOrExit(sizeof(Header) + header->GetPayloadLength() == aDataLen, error = kErrorParse);
-
-    aPriority = DscpToPriority(header->GetDscp());
-
-exit:
-    return error;
-}
-
 Error Ip6::AddMplOption(Message &aMessage, Header &aHeader)
 {
     Error          error = kErrorNone;
@@ -428,10 +415,22 @@
 {
     Error    error = kErrorNone;
     Header   header;
+    uint8_t  dscp;
     uint16_t payloadLength = aMessage.GetLength();
 
+    if ((aIpProto == kProtoUdp) &&
+        Get<Tmf::Agent>().IsTmfMessage(aMessageInfo.GetSockAddr(), aMessageInfo.GetPeerAddr(),
+                                       aMessageInfo.GetPeerPort()))
+    {
+        dscp = Tmf::Agent::PriorityToDscp(aMessage.GetPriority());
+    }
+    else
+    {
+        dscp = PriorityToDscp(aMessage.GetPriority());
+    }
+
     header.InitVersionTrafficClassFlow();
-    header.SetDscp(PriorityToDscp(aMessage.GetPriority()));
+    header.SetDscp(dscp);
     header.SetEcn(aMessageInfo.GetEcn());
     header.SetPayloadLength(payloadLength);
     header.SetNextHeader(aIpProto);
@@ -599,7 +598,7 @@
         offset = fragmentCnt * FragmentHeader::BytesToFragmentOffset(maxPayloadFragment);
         fragmentHeader.SetOffset(offset);
 
-        VerifyOrExit((fragment = NewMessage(0)) != nullptr, error = kErrorNoBufs);
+        VerifyOrExit((fragment = NewMessage()) != nullptr, error = kErrorNoBufs);
         IgnoreError(fragment->SetPriority(aMessage.GetPriority()));
         SuccessOrExit(error = fragment->SetLength(aMessage.GetOffset() + sizeof(fragmentHeader) + payloadFragment));
 
@@ -682,7 +681,7 @@
     if (message == nullptr)
     {
         LogDebg("start reassembly");
-        VerifyOrExit((message = NewMessage(0)) != nullptr, error = kErrorNoBufs);
+        VerifyOrExit((message = NewMessage()) != nullptr, error = kErrorNoBufs);
         mReassemblyList.Enqueue(*message);
 
         message->SetTimestampToNow();
diff --git a/src/core/net/ip6.hpp b/src/core/net/ip6.hpp
index 3c30924..aafb09c 100644
--- a/src/core/net/ip6.hpp
+++ b/src/core/net/ip6.hpp
@@ -136,6 +136,26 @@
     explicit Ip6(Instance &aInstance);
 
     /**
+     * This method allocates a new message buffer from the buffer pool with default settings (link security
+     * enabled and `kPriorityMedium`).
+     *
+     * @returns A pointer to the message or `nullptr` if insufficient message buffers are available.
+     *
+     */
+    Message *NewMessage(void);
+
+    /**
+     * This method allocates a new message buffer from the buffer pool with default settings (link security
+     * enabled and `kPriorityMedium`).
+     *
+     * @param[in]  aReserved  The number of header bytes to reserve following the IPv6 header.
+     *
+     * @returns A pointer to the message or `nullptr` if insufficient message buffers are available.
+     *
+     */
+    Message *NewMessage(uint16_t aReserved);
+
+    /**
      * This method allocates a new message buffer from the buffer pool.
      *
      * @param[in]  aReserved  The number of header bytes to reserve following the IPv6 header.
@@ -144,34 +164,23 @@
      * @returns A pointer to the message or `nullptr` if insufficient message buffers are available.
      *
      */
-    Message *NewMessage(uint16_t aReserved, const Message::Settings &aSettings = Message::Settings::GetDefault());
+    Message *NewMessage(uint16_t aReserved, const Message::Settings &aSettings);
 
     /**
      * This method allocates a new message buffer from the buffer pool and writes the IPv6 datagram to the message.
      *
+     * The message priority is always determined from IPv6 message itself (@p aData) and the priority included in
+     * @p aSetting is ignored.
+     *
      * @param[in]  aData        A pointer to the IPv6 datagram buffer.
      * @param[in]  aDataLength  The size of the IPV6 datagram buffer pointed by @p aData.
      * @param[in]  aSettings    The message settings.
      *
      * @returns A pointer to the message or `nullptr` if malformed IPv6 header or insufficient message buffers are
-     * available.
+     *          available.
      *
      */
-    Message *NewMessage(const uint8_t *aData, uint16_t aDataLength, const Message::Settings &aSettings);
-
-    /**
-     * This method allocates a new message buffer from the buffer pool and writes the IPv6 datagram to the message.
-     *
-     * @note The link layer security is enabled and the message priority is obtained from IPv6 message itself.
-     *
-     * @param[in]  aData        A pointer to the IPv6 datagram buffer.
-     * @param[in]  aDataLength  The size of the IPV6 datagram buffer pointed by @p aData.
-     *
-     * @returns A pointer to the message or `nullptr` if malformed IPv6 header or insufficient message buffers are
-     * available.
-     *
-     */
-    Message *NewMessage(const uint8_t *aData, uint16_t aDataLength);
+    Message *NewMessageFromData(const uint8_t *aData, uint16_t aDataLength, const Message::Settings &aSettings);
 
     /**
      * This method converts the IPv6 DSCP value to message priority level.
@@ -375,7 +384,6 @@
     void HandleSendQueue(void);
 
     static uint8_t PriorityToDscp(Message::Priority aPriority);
-    static Error   GetDatagramPriority(const uint8_t *aData, uint16_t aDataLen, Message::Priority &aPriority);
 
     void  EnqueueDatagram(Message &aMessage);
     Error PassToHost(Message           &aMessage,
diff --git a/src/core/net/ip6_address.cpp b/src/core/net/ip6_address.cpp
index 38b9419..ad97501 100644
--- a/src/core/net/ip6_address.cpp
+++ b/src/core/net/ip6_address.cpp
@@ -96,6 +96,22 @@
            (MatchLength(GetBytes(), aSubPrefix.m8, NetworkPrefix::kSize) >= NetworkPrefix::kLength);
 }
 
+void Prefix::Tidy(void)
+{
+    uint8_t byteLength      = GetBytesSize();
+    uint8_t lastByteBitMask = ~(static_cast<uint8_t>(1 << (byteLength * 8 - mLength)) - 1);
+
+    if (byteLength != 0)
+    {
+        mPrefix.mFields.m8[byteLength - 1] &= lastByteBitMask;
+    }
+
+    for (uint16_t i = byteLength; i < GetArrayLength(mPrefix.mFields.m8); i++)
+    {
+        mPrefix.mFields.m8[i] = 0;
+    }
+}
+
 bool Prefix::operator==(const Prefix &aOther) const
 {
     return (mLength == aOther.mLength) && (MatchLength(GetBytes(), aOther.GetBytes(), GetBytesSize()) >= GetLength());
@@ -157,6 +173,31 @@
            (aLength == 96);
 }
 
+Error Prefix::FromString(const char *aString)
+{
+    constexpr char kSlashChar = '/';
+    constexpr char kNullChar  = '\0';
+
+    Error       error = kErrorParse;
+    const char *cur;
+
+    VerifyOrExit(aString != nullptr);
+
+    cur = StringFind(aString, kSlashChar);
+    VerifyOrExit(cur != nullptr);
+
+    SuccessOrExit(AsCoreType(&mPrefix).ParseFrom(aString, kSlashChar));
+
+    cur++;
+    SuccessOrExit(StringParseUint8(cur, mLength, kMaxLength));
+    VerifyOrExit(*cur == kNullChar);
+
+    error = kErrorNone;
+
+exit:
+    return error;
+}
+
 Prefix::InfoString Prefix::ToString(void) const
 {
     InfoString string;
@@ -176,8 +217,10 @@
 void Prefix::ToString(StringWriter &aWriter) const
 {
     uint8_t sizeInUint16 = (GetBytesSize() + sizeof(uint16_t) - 1) / sizeof(uint16_t);
+    Prefix  tidyPrefix   = *this;
 
-    AsCoreType(&mPrefix).AppendHexWords(aWriter, sizeInUint16);
+    tidyPrefix.Tidy();
+    AsCoreType(&tidyPrefix.mPrefix).AppendHexWords(aWriter, sizeInUint16);
 
     if (GetBytesSize() < Address::kSize - 1)
     {
@@ -492,10 +535,16 @@
 
 Error Address::FromString(const char *aString)
 {
+    constexpr char kNullChar = '\0';
+
+    return ParseFrom(aString, kNullChar);
+}
+
+Error Address::ParseFrom(const char *aString, char aTerminatorChar)
+{
     constexpr uint8_t kInvalidIndex = 0xff;
     constexpr char    kColonChar    = ':';
     constexpr char    kDotChar      = '.';
-    constexpr char    kNullChar     = '\0';
 
     Error   error      = kErrorParse;
     uint8_t index      = 0;
@@ -513,7 +562,7 @@
         colonIndex = index;
     }
 
-    while (*aString != kNullChar)
+    while (*aString != aTerminatorChar)
     {
         const char *start = aString;
         uint32_t    value = 0;
@@ -560,7 +609,7 @@
             break;
         }
 
-        VerifyOrExit((*aString == kColonChar) || (*aString == kNullChar));
+        VerifyOrExit((*aString == kColonChar) || (*aString == aTerminatorChar));
 
         VerifyOrExit(index < endIndex);
         mFields.m16[index++] = HostSwap16(static_cast<uint16_t>(value));
@@ -594,7 +643,7 @@
     {
         Ip4::Address ip4Addr;
 
-        SuccessOrExit(error = ip4Addr.FromString(aString));
+        SuccessOrExit(error = ip4Addr.FromString(aString, aTerminatorChar));
         memcpy(GetArrayEnd(mFields.m8) - Ip4::Address::kSize, ip4Addr.GetBytes(), Ip4::Address::kSize);
     }
 
diff --git a/src/core/net/ip6_address.hpp b/src/core/net/ip6_address.hpp
index 25df8f8..a2542ae 100644
--- a/src/core/net/ip6_address.hpp
+++ b/src/core/net/ip6_address.hpp
@@ -173,6 +173,12 @@
     void SetLength(uint8_t aLength) { mLength = aLength; }
 
     /**
+     * This method sets the bits after the prefix length to 0.
+     *
+     */
+    void Tidy(void);
+
+    /**
      * This method indicates whether prefix length is valid (smaller or equal to max length).
      *
      * @retval TRUE   The prefix length is valid.
@@ -316,6 +322,17 @@
     bool IsValidNat64(void) const { return IsValidNat64PrefixLength(mLength); }
 
     /**
+     * This method parses a given IPv6 prefix string and sets the prefix.
+     *
+     * @param[in]  aString         A null-terminated string, with format "<prefix>/<plen>"
+     *
+     * @retval kErrorNone          Successfully parsed the IPv6 prefix from @p aString.
+     * @retval kErrorParse         Failed to parse the IPv6 prefix from @p aString.
+     *
+     */
+    Error FromString(const char *aString);
+
+    /**
      * This method converts the prefix to a string.
      *
      * The IPv6 prefix string is formatted as "%x:%x:%x:...[::]/plen".
@@ -340,7 +357,8 @@
     void ToString(char *aBuffer, uint16_t aSize) const;
 
 private:
-    void ToString(StringWriter &aWriter) const;
+    uint8_t ByteAfterTidy(uint8_t aIndex);
+    void    ToString(StringWriter &aWriter) const;
 } OT_TOOL_PACKED_END;
 
 /**
@@ -1028,6 +1046,8 @@
 
     static void CopyBits(uint8_t *aDst, const uint8_t *aSrc, uint8_t aNumBits);
 
+    Error ParseFrom(const char *aString, char aTerminatorChar);
+
 } OT_TOOL_PACKED_END;
 
 /**
diff --git a/src/core/net/ip6_types.hpp b/src/core/net/ip6_types.hpp
index 06af126..8e1f9c3 100644
--- a/src/core/net/ip6_types.hpp
+++ b/src/core/net/ip6_types.hpp
@@ -89,7 +89,13 @@
     kDscpCs5    = 40,   ///< Class selector codepoint 40
     kDscpCs6    = 48,   ///< Class selector codepoint 48
     kDscpCs7    = 56,   ///< Class selector codepoint 56
-    kDscpCsMask = 0x38, ///< Class selector mask
+    kDscpCsMask = 0x38, ///< Class selector mask (0b111000)
+
+    // DSCP values to use within Thread mesh (from local codepoint space 0bxxxx11 [RFC 2474 - section 6]).
+
+    kDscpTmfNetPriority    = 0x07, ///< TMF network priority (0b000111).
+    kDscpTmfNormalPriority = 0x0f, ///< TMF normal priority  (0b001111).
+    kDscpTmfLowPriority    = 0x17, ///< TMF low priority     (0b010111).
 };
 
 /**
diff --git a/src/core/net/nat64_translator.cpp b/src/core/net/nat64_translator.cpp
index d642aa3..ee9cd2d 100644
--- a/src/core/net/nat64_translator.cpp
+++ b/src/core/net/nat64_translator.cpp
@@ -414,7 +414,7 @@
     {
     case Ip4::Icmp::Header::Type::kTypeEchoReply:
     {
-        // The only difference between ICMPv6 echo and ICMP4 echo is the message type field, so we can reinteprete it as
+        // The only difference between ICMPv6 echo and ICMP4 echo is the message type field, so we can reinterpret it as
         // ICMP6 header and set the message type.
         SuccessOrExit(err = aMessage.Read(0, icmp6Header));
         icmp6Header.SetType(Ip6::Icmp::Header::Type::kTypeEchoReply);
@@ -444,7 +444,7 @@
     {
     case Ip6::Icmp::Header::Type::kTypeEchoRequest:
     {
-        // The only difference between ICMPv6 echo and ICMP4 echo is the message type field, so we can reinteprete it as
+        // The only difference between ICMPv6 echo and ICMP4 echo is the message type field, so we can reinterpret it as
         // ICMP6 header and set the message type.
         SuccessOrExit(err = aMessage.Read(0, icmp4Header));
         icmp4Header.SetType(Ip4::Icmp::Header::Type::kTypeEchoRequest);
diff --git a/src/core/net/nat64_translator.hpp b/src/core/net/nat64_translator.hpp
index b670aa1..1ec16a4 100644
--- a/src/core/net/nat64_translator.hpp
+++ b/src/core/net/nat64_translator.hpp
@@ -215,7 +215,7 @@
      * @param[in,out] aMessage the message to be processed.
      *
      * @retval kNotTranslated The message is already an IPv6 datagram. @p aMessage is not updated.
-     * @retval kForward       The caller should contiue forwarding the datagram.
+     * @retval kForward       The caller should continue forwarding the datagram.
      * @retval kDrop          The caller should drop the datagram silently.
      *
      */
@@ -229,7 +229,7 @@
      * @param[in,out] aMessage the message to be processed.
      *
      * @retval kNotTranslated The datagram is not sending to the configured NAT64 prefix.
-     * @retval kForward       The caller should contiue forwarding the datagram.
+     * @retval kForward       The caller should continue forwarding the datagram.
      * @retval kDrop          The caller should drop the datagram silently.
      *
      */
diff --git a/src/core/net/srp_client.cpp b/src/core/net/srp_client.cpp
index a1c8c1c..d964731 100644
--- a/src/core/net/srp_client.cpp
+++ b/src/core/net/srp_client.cpp
@@ -355,7 +355,7 @@
 
 #if OPENTHREAD_CONFIG_SRP_CLIENT_AUTO_START_API_ENABLE
 #if OPENTHREAD_CONFIG_SRP_CLIENT_SWITCH_SERVER_ON_FAILURE
-    mAutoStart.ResetTimoutFailureCount();
+    mAutoStart.ResetTimeoutFailureCount();
 #endif
     if (aRequester == kRequesterAuto)
     {
@@ -744,7 +744,7 @@
     };
 
     Error    error   = kErrorNone;
-    Message *message = mSocket.NewMessage(0);
+    Message *message = mSocket.NewMessage();
     uint32_t length;
 
     VerifyOrExit(message != nullptr, error = kErrorNoBufs);
@@ -835,7 +835,12 @@
 
     info.Clear();
 
+#if OPENTHREAD_CONFIG_PLATFORM_KEY_REFERENCES_ENABLE
+    info.mKeyRef.SetKeyRef(kSrpEcdsaKeyRef);
+    SuccessOrExit(error = ReadOrGenerateKey(info.mKeyRef));
+#else
     SuccessOrExit(error = ReadOrGenerateKey(info.mKeyPair));
+#endif
 
     // Generate random Message ID and ensure it is different from last one
     do
@@ -885,6 +890,34 @@
     return error;
 }
 
+#if OPENTHREAD_CONFIG_PLATFORM_KEY_REFERENCES_ENABLE
+Error Client::ReadOrGenerateKey(Crypto::Ecdsa::P256::KeyPairAsRef &aKeyRef)
+{
+    Error                        error = kErrorNone;
+    Crypto::Ecdsa::P256::KeyPair keyPair;
+
+    VerifyOrExit(!Crypto::Storage::HasKey(aKeyRef.GetKeyRef()));
+    error = Get<Settings>().Read<Settings::SrpEcdsaKey>(keyPair);
+
+    if (error == kErrorNone)
+    {
+        Crypto::Ecdsa::P256::PublicKey publicKey;
+
+        if (keyPair.GetPublicKey(publicKey) == kErrorNone)
+        {
+            SuccessOrExit(error = aKeyRef.ImportKeyPair(keyPair));
+            IgnoreError(Get<Settings>().Delete<Settings::SrpEcdsaKey>());
+            ExitNow();
+        }
+        IgnoreError(Get<Settings>().Delete<Settings::SrpEcdsaKey>());
+    }
+
+    error = aKeyRef.Generate();
+
+exit:
+    return error;
+}
+#else
 Error Client::ReadOrGenerateKey(Crypto::Ecdsa::P256::KeyPair &aKeyPair)
 {
     Error error;
@@ -907,6 +940,7 @@
 exit:
     return error;
 }
+#endif //  OPENTHREAD_CONFIG_PLATFORM_KEY_REFERENCES_ENABLE
 
 Error Client::AppendServiceInstructions(Message &aMessage, Info &aInfo)
 {
@@ -1281,7 +1315,11 @@
     key.SetAlgorithm(Dns::KeyRecord::kAlgorithmEcdsaP256Sha256);
     key.SetLength(sizeof(Dns::KeyRecord) - sizeof(Dns::ResourceRecord) + sizeof(Crypto::Ecdsa::P256::PublicKey));
     SuccessOrExit(error = aMessage.Append(key));
+#if OPENTHREAD_CONFIG_PLATFORM_KEY_REFERENCES_ENABLE
+    SuccessOrExit(error = aInfo.mKeyRef.GetPublicKey(publicKey));
+#else
     SuccessOrExit(error = aInfo.mKeyPair.GetPublicKey(publicKey));
+#endif
     SuccessOrExit(error = aMessage.Append(publicKey));
     aInfo.mRecordCount++;
 
@@ -1418,7 +1456,11 @@
     sha256.Update(aMessage, 0, offset);
 
     sha256.Finish(hash);
+#if OPENTHREAD_CONFIG_PLATFORM_KEY_REFERENCES_ENABLE
+    SuccessOrExit(error = aInfo.mKeyRef.Sign(hash, signature));
+#else
     SuccessOrExit(error = aInfo.mKeyPair.Sign(hash, signature));
+#endif
 
     // Move back in message and append SIG RR now with compressed host
     // name (as signer's name) along with the calculated signature.
@@ -1494,7 +1536,7 @@
     LogInfo("Received response");
 
 #if OPENTHREAD_CONFIG_SRP_CLIENT_AUTO_START_API_ENABLE && OPENTHREAD_CONFIG_SRP_CLIENT_SWITCH_SERVER_ON_FAILURE
-    mAutoStart.ResetTimoutFailureCount();
+    mAutoStart.ResetTimeoutFailureCount();
 #endif
 
     error = Dns::Header::ResponseCodeToError(header.GetResponseCode());
@@ -1888,9 +1930,9 @@
         // callback. It works correctly due to the guard check at the
         // top of `SelectNextServer()`.
 
-        mAutoStart.IncrementTimoutFailureCount();
+        mAutoStart.IncrementTimeoutFailureCount();
 
-        if (mAutoStart.GetTimoutFailureCount() >= kMaxTimeoutFailuresToSwitchServer)
+        if (mAutoStart.GetTimeoutFailureCount() >= kMaxTimeoutFailuresToSwitchServer)
         {
             SelectNextServer(kDisallowSwitchOnRegisteredHost);
         }
diff --git a/src/core/net/srp_client.hpp b/src/core/net/srp_client.hpp
index 5c5f1d7..0aaf632 100644
--- a/src/core/net/srp_client.hpp
+++ b/src/core/net/srp_client.hpp
@@ -794,6 +794,10 @@
     // Number of fast data polls after SRP Update tx (11x 188ms = ~2 seconds)
     static constexpr uint8_t kFastPollsAfterUpdateTx = 11;
 
+#if OPENTHREAD_CONFIG_PLATFORM_KEY_REFERENCES_ENABLE
+    static constexpr uint32_t kSrpEcdsaKeyRef = Crypto::Storage::kEcdsaRef;
+#endif
+
 #if OPENTHREAD_CONFIG_SRP_CLIENT_SWITCH_SERVER_ON_FAILURE
     static constexpr uint8_t kMaxTimeoutFailuresToSwitchServer =
         OPENTHREAD_CONFIG_SRP_CLIENT_MAX_TIMEOUT_FAILURES_TO_SWITCH_SERVER;
@@ -854,7 +858,7 @@
     // will retry after a short interval `kTxFailureRetryInterval`
     // up to `kMaxTxFailureRetries` attempts. After this, the retry
     // wait interval will be used (which keeps growing on each failure
-    // - please see bellow).
+    // - please see below).
     //
     // If the update message is sent successfully but there is no
     // response from server or if server rejects the update, the
@@ -952,13 +956,13 @@
         void    InvokeCallback(const Ip6::SockAddr *aServerSockAddr) const;
 
 #if OPENTHREAD_CONFIG_SRP_CLIENT_SWITCH_SERVER_ON_FAILURE
-        uint8_t GetTimoutFailureCount(void) const { return mTimoutFailureCount; }
-        void    ResetTimoutFailureCount(void) { mTimoutFailureCount = 0; }
-        void    IncrementTimoutFailureCount(void)
+        uint8_t GetTimeoutFailureCount(void) const { return mTimeoutFailureCount; }
+        void    ResetTimeoutFailureCount(void) { mTimeoutFailureCount = 0; }
+        void    IncrementTimeoutFailureCount(void)
         {
-            if (mTimoutFailureCount < NumericLimits<uint8_t>::kMax)
+            if (mTimeoutFailureCount < NumericLimits<uint8_t>::kMax)
             {
-                mTimoutFailureCount++;
+                mTimeoutFailureCount++;
             }
         }
 #endif
@@ -972,7 +976,7 @@
         State                       mState;
         uint8_t                     mAnycastSeqNum;
 #if OPENTHREAD_CONFIG_SRP_CLIENT_SWITCH_SERVER_ON_FAILURE
-        uint8_t mTimoutFailureCount; // Number of no-response timeout failures with the currently selected server.
+        uint8_t mTimeoutFailureCount; // Number of no-response timeout failures with the currently selected server.
 #endif
     };
 #endif // OPENTHREAD_CONFIG_SRP_CLIENT_AUTO_START_API_ENABLE
@@ -981,29 +985,37 @@
     {
         static constexpr uint16_t kUnknownOffset = 0; // Unknown offset value (used when offset is not yet set).
 
-        uint16_t                     mDomainNameOffset; // Offset of domain name serialization
-        uint16_t                     mHostNameOffset;   // Offset of host name serialization.
-        uint16_t                     mRecordCount;      // Number of resource records in Update section.
-        Crypto::Ecdsa::P256::KeyPair mKeyPair;          // The ECDSA key pair.
+        uint16_t mDomainNameOffset; // Offset of domain name serialization
+        uint16_t mHostNameOffset;   // Offset of host name serialization.
+        uint16_t mRecordCount;      // Number of resource records in Update section.
+#if OPENTHREAD_CONFIG_PLATFORM_KEY_REFERENCES_ENABLE
+        Crypto::Ecdsa::P256::KeyPairAsRef mKeyRef; // The ECDSA key ref for key-pair.
+#else
+        Crypto::Ecdsa::P256::KeyPair mKeyPair; // The ECDSA key pair.
+#endif
     };
 
-    Error        Start(const Ip6::SockAddr &aServerSockAddr, Requester aRequester);
-    void         Stop(Requester aRequester, StopMode aMode);
-    void         Resume(void);
-    void         Pause(void);
-    void         HandleNotifierEvents(Events aEvents);
-    void         HandleRoleChanged(void);
-    Error        UpdateHostInfoStateOnAddressChange(void);
-    void         UpdateServiceStateToRemove(Service &aService);
-    State        GetState(void) const { return mState; }
-    void         SetState(State aState);
-    void         ChangeHostAndServiceStates(const ItemState *aNewStates, ServiceStateChangeMode aMode);
-    void         InvokeCallback(Error aError) const;
-    void         InvokeCallback(Error aError, const HostInfo &aHostInfo, const Service *aRemovedServices) const;
-    void         HandleHostInfoOrServiceChange(void);
-    void         SendUpdate(void);
-    Error        PrepareUpdateMessage(Message &aMessage);
-    Error        ReadOrGenerateKey(Crypto::Ecdsa::P256::KeyPair &aKeyPair);
+    Error Start(const Ip6::SockAddr &aServerSockAddr, Requester aRequester);
+    void  Stop(Requester aRequester, StopMode aMode);
+    void  Resume(void);
+    void  Pause(void);
+    void  HandleNotifierEvents(Events aEvents);
+    void  HandleRoleChanged(void);
+    Error UpdateHostInfoStateOnAddressChange(void);
+    void  UpdateServiceStateToRemove(Service &aService);
+    State GetState(void) const { return mState; }
+    void  SetState(State aState);
+    void  ChangeHostAndServiceStates(const ItemState *aNewStates, ServiceStateChangeMode aMode);
+    void  InvokeCallback(Error aError) const;
+    void  InvokeCallback(Error aError, const HostInfo &aHostInfo, const Service *aRemovedServices) const;
+    void  HandleHostInfoOrServiceChange(void);
+    void  SendUpdate(void);
+    Error PrepareUpdateMessage(Message &aMessage);
+#if OPENTHREAD_CONFIG_PLATFORM_KEY_REFERENCES_ENABLE
+    Error ReadOrGenerateKey(Crypto::Ecdsa::P256::KeyPairAsRef &aKeyRef);
+#else
+    Error ReadOrGenerateKey(Crypto::Ecdsa::P256::KeyPair &aKeyPair);
+#endif
     Error        AppendServiceInstructions(Message &aMessage, Info &aInfo);
     bool         CanAppendService(const Service &aService);
     Error        AppendServiceInstruction(Service &aService, Message &aMessage, Info &aInfo);
diff --git a/src/core/net/srp_server.cpp b/src/core/net/srp_server.cpp
index f243d16..fc4dbc3 100644
--- a/src/core/net/srp_server.cpp
+++ b/src/core/net/srp_server.cpp
@@ -1202,7 +1202,7 @@
 
     // The uncompressed (canonical) form of the signer name should be used for signature
     // verification. See https://tools.ietf.org/html/rfc2931#section-3.1 for details.
-    signerNameMessage = Get<Ip6::Udp>().NewMessage(0);
+    signerNameMessage = Get<Ip6::Udp>().NewMessage();
     VerifyOrExit(signerNameMessage != nullptr, error = kErrorNoBufs);
     SuccessOrExit(error = Dns::Name::AppendName(aSignerName, *signerNameMessage));
     sha256.Update(*signerNameMessage, signerNameMessage->GetOffset(), signerNameMessage->GetLength());
@@ -1399,7 +1399,7 @@
     Message          *response = nullptr;
     Dns::UpdateHeader header;
 
-    response = GetSocket().NewMessage(0);
+    response = GetSocket().NewMessage();
     VerifyOrExit(response != nullptr, error = kErrorNoBufs);
 
     header.SetMessageId(aHeader.GetMessageId());
@@ -1442,7 +1442,7 @@
     Dns::LeaseOption  leaseOption;
     uint16_t          optionSize;
 
-    response = GetSocket().NewMessage(0);
+    response = GetSocket().NewMessage();
     VerifyOrExit(response != nullptr, error = kErrorNoBufs);
 
     header.SetMessageId(aHeader.GetMessageId());
@@ -1994,7 +1994,7 @@
 {
     // This method processes the TTL value received in a resource record.
     //
-    // If no TTL value is stored, this method wil set the stored value to @p aTtl and return `kErrorNone`.
+    // If no TTL value is stored, this method will set the stored value to @p aTtl and return `kErrorNone`.
     // If a TTL value is stored and @p aTtl equals the stored value, this method returns `kErrorNone`.
     // Otherwise, this method returns `kErrorRejected`.
 
@@ -2149,7 +2149,7 @@
 
         if (service.mIsDeleted)
         {
-            // `RemoveService()` does nothing if `exitsingService` is `nullptr`.
+            // `RemoveService()` does nothing if `existingService` is `nullptr`.
             RemoveService(existingService, kRetainName, kDoNotNotifyServiceHandler);
             continue;
         }
diff --git a/src/core/net/srp_server.hpp b/src/core/net/srp_server.hpp
index 9473b06..b88b136 100644
--- a/src/core/net/srp_server.hpp
+++ b/src/core/net/srp_server.hpp
@@ -832,7 +832,7 @@
      * disabled by a call to `SetEnabled()` method. Disabling auto-enable mode using `SetAutoEnableMode(false` call
      * will not change the current state of SRP sever (e.g., if it is enabled it stays enabled).
      *
-     * @param[in] aEnbaled    A boolean to enable/disable the auto-enable mode.
+     * @param[in] aEnabled    A boolean to enable/disable the auto-enable mode.
      *
      */
     void SetAutoEnableMode(bool aEnabled);
@@ -929,11 +929,11 @@
     static constexpr uint32_t kDefaultEventsHandlerTimeout = OPENTHREAD_CONFIG_SRP_SERVER_SERVICE_UPDATE_TIMEOUT;
 
     static constexpr AddressMode kDefaultAddressMode =
-        static_cast<AddressMode>(OPENTHREAD_CONFIG_SRP_SERVER_DEFAULT_ADDDRESS_MODE);
+        static_cast<AddressMode>(OPENTHREAD_CONFIG_SRP_SERVER_DEFAULT_ADDRESS_MODE);
 
     static constexpr uint16_t kAnycastAddressModePort = 53;
 
-    // Metdata for a received SRP Update message.
+    // Metadata for a received SRP Update message.
     struct MessageMetadata
     {
         // Indicates whether the `Message` is received directly from a
diff --git a/src/core/net/udp6.cpp b/src/core/net/udp6.cpp
index 9e2adaa..3ddda61 100644
--- a/src/core/net/udp6.cpp
+++ b/src/core/net/udp6.cpp
@@ -77,6 +77,10 @@
     Clear();
 }
 
+Message *Udp::Socket::NewMessage(void) { return NewMessage(0); }
+
+Message *Udp::Socket::NewMessage(uint16_t aReserved) { return NewMessage(aReserved, Message::Settings::GetDefault()); }
+
 Message *Udp::Socket::NewMessage(uint16_t aReserved, const Message::Settings &aSettings)
 {
     return Get<Udp>().NewMessage(aReserved, aSettings);
@@ -414,6 +418,10 @@
     return mEphemeralPort;
 }
 
+Message *Udp::NewMessage(void) { return NewMessage(0); }
+
+Message *Udp::NewMessage(uint16_t aReserved) { return NewMessage(aReserved, Message::Settings::GetDefault()); }
+
 Message *Udp::NewMessage(uint16_t aReserved, const Message::Settings &aSettings)
 {
     return Get<Ip6>().NewMessage(sizeof(Header) + aReserved, aSettings);
diff --git a/src/core/net/udp6.hpp b/src/core/net/udp6.hpp
index 911a026..6648f00 100644
--- a/src/core/net/udp6.hpp
+++ b/src/core/net/udp6.hpp
@@ -162,6 +162,24 @@
         explicit Socket(Instance &aInstance);
 
         /**
+         * This method returns a new UDP message with default settings (link security enabled and `kPriorityNormal`)
+         *
+         * @returns A pointer to the message or `nullptr` if no buffers are available.
+         *
+         */
+        Message *NewMessage(void);
+
+        /**
+         * This method returns a new UDP message with default settings (link security enabled and `kPriorityNormal`)
+         *
+         * @param[in]  aReserved  The number of header bytes to reserve after the UDP header.
+         *
+         * @returns A pointer to the message or `nullptr` if no buffers are available.
+         *
+         */
+        Message *NewMessage(uint16_t aReserved);
+
+        /**
          * This method returns a new UDP message with sufficient header space reserved.
          *
          * @param[in]  aReserved  The number of header bytes to reserve after the UDP header.
@@ -170,7 +188,7 @@
          * @returns A pointer to the message or `nullptr` if no buffers are available.
          *
          */
-        Message *NewMessage(uint16_t aReserved, const Message::Settings &aSettings = Message::Settings::GetDefault());
+        Message *NewMessage(uint16_t aReserved, const Message::Settings &aSettings);
 
         /**
          * This method opens the UDP socket.
@@ -534,6 +552,24 @@
     uint16_t GetEphemeralPort(void);
 
     /**
+     * This method returns a new UDP message with default settings (link security enabled and `kPriorityNormal`)
+     *
+     * @returns A pointer to the message or `nullptr` if no buffers are available.
+     *
+     */
+    Message *NewMessage(void);
+
+    /**
+     * This method returns a new UDP message with default settings (link security enabled and `kPriorityNormal`)
+     *
+     * @param[in]  aReserved  The number of header bytes to reserve after the UDP header.
+     *
+     * @returns A pointer to the message or `nullptr` if no buffers are available.
+     *
+     */
+    Message *NewMessage(uint16_t aReserved);
+
+    /**
      * This method returns a new UDP message with sufficient header space reserved.
      *
      * @param[in]  aReserved  The number of header bytes to reserve after the UDP header.
@@ -542,7 +578,7 @@
      * @returns A pointer to the message or `nullptr` if no buffers are available.
      *
      */
-    Message *NewMessage(uint16_t aReserved, const Message::Settings &aSettings = Message::Settings::GetDefault());
+    Message *NewMessage(uint16_t aReserved, const Message::Settings &aSettings);
 
     /**
      * This method sends an IPv6 datagram.
diff --git a/src/core/openthread-core-config.h b/src/core/openthread-core-config.h
index b036243..2b4127d 100644
--- a/src/core/openthread-core-config.h
+++ b/src/core/openthread-core-config.h
@@ -88,6 +88,7 @@
 #include "config/mle.h"
 #include "config/nat64.h"
 #include "config/netdata_publisher.h"
+#include "config/network_diagnostic.h"
 #include "config/parent_search.h"
 #include "config/ping_sender.h"
 #include "config/platform.h"
diff --git a/src/core/radio/trel_interface.hpp b/src/core/radio/trel_interface.hpp
index 2181915..1822d47 100644
--- a/src/core/radio/trel_interface.hpp
+++ b/src/core/radio/trel_interface.hpp
@@ -256,12 +256,12 @@
     Peer *GetNewPeerEntry(void);
     void  RemovePeerEntry(Peer &aEntry);
 
-    using RegsiterServiceTask = TaskletIn<Interface, &Interface::RegisterService>;
+    using RegisterServiceTask = TaskletIn<Interface, &Interface::RegisterService>;
 
     bool                mInitialized : 1;
     bool                mEnabled : 1;
     bool                mFiltered : 1;
-    RegsiterServiceTask mRegisterServiceTask;
+    RegisterServiceTask mRegisterServiceTask;
     uint16_t            mUdpPort;
     Packet              mRxPacket;
     PeerTable           mPeerTable;
diff --git a/src/core/radio/trel_link.cpp b/src/core/radio/trel_link.cpp
index f563501..8eb2b9e 100644
--- a/src/core/radio/trel_link.cpp
+++ b/src/core/radio/trel_link.cpp
@@ -121,9 +121,9 @@
     Mac::PanId    destPanId;
     Header::Type  type;
     Packet        txPacket;
-    Neighbor     *neighbor   = nullptr;
-    Mac::RxFrame *ackFrame   = nullptr;
-    bool          isDisovery = false;
+    Neighbor     *neighbor    = nullptr;
+    Mac::RxFrame *ackFrame    = nullptr;
+    bool          isDiscovery = false;
 
     VerifyOrExit(mState == kStateTransmit);
 
@@ -170,14 +170,14 @@
 
         if (!mTxFrame.GetSecurityEnabled())
         {
-            isDisovery = true;
+            isDiscovery = true;
         }
         else
         {
             uint8_t keyIdMode;
 
             IgnoreError(mTxFrame.GetKeyIdMode(keyIdMode));
-            isDisovery = (keyIdMode == Mac::Frame::kKeyIdMode2);
+            isDiscovery = (keyIdMode == Mac::Frame::kKeyIdMode2);
         }
     }
 
@@ -212,7 +212,7 @@
 
     LogDebg("BeginTransmit() [%s] plen:%d", txPacket.GetHeader().ToString().AsCString(), txPacket.GetPayloadLength());
 
-    VerifyOrExit(mInterface.Send(txPacket, isDisovery) == kErrorNone, InvokeSendDone(kErrorAbort));
+    VerifyOrExit(mInterface.Send(txPacket, isDiscovery) == kErrorNone, InvokeSendDone(kErrorAbort));
 
     if (mTxFrame.GetAckRequest())
     {
diff --git a/src/core/thread/address_resolver.cpp b/src/core/thread/address_resolver.cpp
index 760b42e..16bf8e7 100644
--- a/src/core/thread/address_resolver.cpp
+++ b/src/core/thread/address_resolver.cpp
@@ -163,12 +163,12 @@
     return error;
 }
 
-void AddressResolver::Remove(uint8_t aRouterId)
+void AddressResolver::RemoveEntriesForRouterId(uint8_t aRouterId)
 {
     Remove(Mle::Rloc16FromRouterId(aRouterId), /* aMatchRouterId */ true);
 }
 
-void AddressResolver::Remove(uint16_t aRloc16) { Remove(aRloc16, /* aMatchRouterId */ false); }
+void AddressResolver::RemoveEntriesForRloc16(uint16_t aRloc16) { Remove(aRloc16, /* aMatchRouterId */ false); }
 
 AddressResolver::CacheEntry *AddressResolver::GetEntryAfter(CacheEntry *aPrev, CacheEntryList &aList)
 {
@@ -221,7 +221,7 @@
     return entry;
 }
 
-void AddressResolver::Remove(const Ip6::Address &aEid) { Remove(aEid, kReasonRemovingEid); }
+void AddressResolver::RemoveEntryForAddress(const Ip6::Address &aEid) { Remove(aEid, kReasonRemovingEid); }
 
 void AddressResolver::Remove(const Ip6::Address &aEid, Reason aReason)
 {
@@ -239,6 +239,22 @@
     return;
 }
 
+void AddressResolver::ReplaceEntriesForRloc16(uint16_t aOldRloc16, uint16_t aNewRloc16)
+{
+    CacheEntryList *lists[] = {&mCachedList, &mSnoopedList};
+
+    for (CacheEntryList *list : lists)
+    {
+        for (CacheEntry &entry : *list)
+        {
+            if (entry.GetRloc16() == aOldRloc16)
+            {
+                entry.SetRloc16(aNewRloc16);
+            }
+        }
+    }
+}
+
 AddressResolver::CacheEntry *AddressResolver::NewCacheEntry(bool aSnoopedEntry)
 {
     CacheEntry     *newEntry  = nullptr;
@@ -600,7 +616,7 @@
 
     SuccessOrExit(error = Get<Tmf::Agent>().SendMessage(*message, messageInfo));
 
-    LogInfo("Sending address query for %s", aEid.ToString().AsCString());
+    LogInfo("Sent %s for %s", UriToString<kUriAddressQuery>(), aEid.ToString().AsCString());
 
 exit:
 
@@ -612,8 +628,8 @@
     {
         uint16_t selfRloc16 = Get<Mle::MleRouter>().GetRloc16();
 
-        LogInfo("Extending ADDR.qry to BB.qry for target=%s, rloc16=%04x(self)", aEid.ToString().AsCString(),
-                selfRloc16);
+        LogInfo("Extending %s to %s for target %s, rloc16=%04x(self)", UriToString<kUriAddressQuery>(),
+                UriToString<kUriBackboneQuery>(), aEid.ToString().AsCString(), selfRloc16);
         IgnoreError(Get<BackboneRouter::Manager>().SendBackboneQuery(aEid, selfRloc16));
     }
 #endif
@@ -649,7 +665,7 @@
         ExitNow();
     }
 
-    LogInfo("Received address notification from 0x%04x for %s to 0x%04x",
+    LogInfo("Received %s from 0x%04x for %s to 0x%04x", UriToString<kUriAddressNotify>(),
             aMessageInfo.GetPeerAddr().GetIid().GetLocator(), target.ToString().AsCString(), rloc16);
 
     entry = FindCacheEntry(target, list, prev);
@@ -681,7 +697,7 @@
 
     if (Get<Tmf::Agent>().SendEmptyAck(aMessage, aMessageInfo) == kErrorNone)
     {
-        LogInfo("Sending address notification acknowledgment");
+        LogInfo("Sent %s ack", UriToString<kUriAddressNotify>());
     }
 
     Get<MeshForwarder>().HandleResolved(target, kErrorNone);
@@ -718,14 +734,14 @@
 
     SuccessOrExit(error = Get<Tmf::Agent>().SendMessage(*message, messageInfo));
 
-    LogInfo("Sending address error for target %s", aTarget.ToString().AsCString());
+    LogInfo("Sent %s for target %s", UriToString<kUriAddressError>(), aTarget.ToString().AsCString());
 
 exit:
 
     if (error != kErrorNone)
     {
         FreeMessage(message);
-        LogInfo("Failed to send address error: %s", ErrorToString(error));
+        LogInfo("Failed to send %s: %s", UriToString<kUriAddressError>(), ErrorToString(error));
     }
 }
 
@@ -744,13 +760,13 @@
 
     VerifyOrExit(aMessage.IsPostRequest(), error = kErrorDrop);
 
-    LogInfo("Received address error notification");
+    LogInfo("Received %s", UriToString<kUriAddressError>());
 
     if (aMessage.IsConfirmable() && !aMessageInfo.GetSockAddr().IsMulticast())
     {
         if (Get<Tmf::Agent>().SendEmptyAck(aMessage, aMessageInfo) == kErrorNone)
         {
-            LogInfo("Sent address error notification acknowledgment");
+            LogInfo("Sent %s ack", UriToString<kUriAddressError>());
         }
     }
 
@@ -807,7 +823,7 @@
 
     if (error != kErrorNone)
     {
-        LogWarn("Error while processing address error notification: %s", ErrorToString(error));
+        LogWarn("Error %s when processing %s", ErrorToString(error), UriToString<kUriAddressError>());
     }
 }
 
@@ -823,8 +839,8 @@
 
     SuccessOrExit(Tlv::Find<ThreadTargetTlv>(aMessage, target));
 
-    LogInfo("Received address query from 0x%04x for target %s", aMessageInfo.GetPeerAddr().GetIid().GetLocator(),
-            target.ToString().AsCString());
+    LogInfo("Received %s from 0x%04x for target %s", UriToString<kUriAddressQuery>(),
+            aMessageInfo.GetPeerAddr().GetIid().GetLocator(), target.ToString().AsCString());
 
     if (Get<ThreadNetif>().HasUnicastAddress(target))
     {
@@ -853,7 +869,8 @@
     {
         uint16_t srcRloc16 = aMessageInfo.GetPeerAddr().GetIid().GetLocator();
 
-        LogInfo("Extending ADDR.qry to BB.qry for target=%s, rloc16=%04x", target.ToString().AsCString(), srcRloc16);
+        LogInfo("Extending %s to %s for target %s rloc16=%04x", UriToString<kUriAddressQuery>(),
+                UriToString<kUriBackboneQuery>(), target.ToString().AsCString(), srcRloc16);
         IgnoreError(Get<BackboneRouter::Manager>().SendBackboneQuery(target, srcRloc16));
     }
 #endif
@@ -887,7 +904,7 @@
 
     SuccessOrExit(error = Get<Tmf::Agent>().SendMessage(*message, messageInfo));
 
-    LogInfo("Sending address notification for target %s", aTarget.ToString().AsCString());
+    LogInfo("Sent %s for target %s", UriToString<kUriAddressNotify>(), aTarget.ToString().AsCString());
 
 exit:
     FreeMessageOnError(message, error);
@@ -955,7 +972,7 @@
                 mQueryList.PopAfter(prev);
                 mQueryRetryList.Push(*entry);
 
-                LogInfo("Timed out waiting for address notification for %s, retry: %d",
+                LogInfo("Timed out waiting for %s for %s, retry: %d", UriToString<kUriAddressNotify>(),
                         entry->GetTarget().ToString().AsCString(), entry->GetTimeout());
 
                 Get<MeshForwarder>().HandleResolved(entry->GetTarget(), kErrorDrop);
diff --git a/src/core/thread/address_resolver.hpp b/src/core/thread/address_resolver.hpp
index 9c45df6..81ee36b 100644
--- a/src/core/thread/address_resolver.hpp
+++ b/src/core/thread/address_resolver.hpp
@@ -139,7 +139,7 @@
      * @param[in]  aRloc16  The RLOC16 address.
      *
      */
-    void Remove(Mac::ShortAddress aRloc16);
+    void RemoveEntriesForRloc16(Mac::ShortAddress aRloc16);
 
     /**
      * This method removes all EID-to-RLOC cache entries associated with a Router ID.
@@ -147,7 +147,7 @@
      * @param[in]  aRouterId  The Router ID.
      *
      */
-    void Remove(uint8_t aRouterId);
+    void RemoveEntriesForRouterId(uint8_t aRouterId);
 
     /**
      * This method removes the cache entry for the EID.
@@ -155,7 +155,16 @@
      * @param[in]  aEid               A reference to the EID.
      *
      */
-    void Remove(const Ip6::Address &aEid);
+    void RemoveEntryForAddress(const Ip6::Address &aEid);
+
+    /**
+     * This method replaces all EID-to-RLOC cache entries corresponding to an old RLOC16 with a new RLOC16.
+     *
+     * @param[in] aOldRloc16    The old RLOC16.
+     * @param[in] aNewRloc16    The new RLOC16.
+     *
+     */
+    void ReplaceEntriesForRloc16(uint16_t aOldRloc16, uint16_t aNewRloc16);
 
     /**
      * This method updates an existing entry or adds a snooped cache entry for a given EID.
diff --git a/src/core/thread/announce_begin_server.cpp b/src/core/thread/announce_begin_server.cpp
index ba22e02..1c904e3 100644
--- a/src/core/thread/announce_begin_server.cpp
+++ b/src/core/thread/announce_begin_server.cpp
@@ -66,10 +66,9 @@
 template <>
 void AnnounceBeginServer::HandleTmf<kUriAnnounceBegin>(Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo)
 {
-    uint32_t         mask;
-    uint8_t          count;
-    uint16_t         period;
-    Ip6::MessageInfo responseInfo(aMessageInfo);
+    uint32_t mask;
+    uint8_t  count;
+    uint16_t period;
 
     VerifyOrExit(aMessage.IsPostRequest());
     VerifyOrExit((mask = MeshCoP::ChannelMaskTlv::GetChannelMask(aMessage)) != 0);
@@ -81,8 +80,8 @@
 
     if (aMessage.IsConfirmable() && !aMessageInfo.GetSockAddr().IsMulticast())
     {
-        SuccessOrExit(Get<Tmf::Agent>().SendEmptyAck(aMessage, responseInfo));
-        LogInfo("Sent announce begin response");
+        SuccessOrExit(Get<Tmf::Agent>().SendEmptyAck(aMessage, aMessageInfo));
+        LogInfo("Sent %s response", UriToString<kUriAnnounceBegin>());
     }
 
 exit:
diff --git a/src/core/thread/csl_tx_scheduler.cpp b/src/core/thread/csl_tx_scheduler.cpp
index 9d86408..f30f85f 100644
--- a/src/core/thread/csl_tx_scheduler.cpp
+++ b/src/core/thread/csl_tx_scheduler.cpp
@@ -227,7 +227,7 @@
     // in `RescheduleCslTx()` when determining the next CSL delay to
     // schedule CSL tx with `Mac` but here we calculate the delay with
     // zero `aAheadUs`. All the timings are in usec but when passing
-    // delay to `Mac` we divide by `1000` (to covert to msec) which
+    // delay to `Mac` we divide by `1000` (to convert to msec) which
     // can round the value down and cause `Mac` to start operation a
     // bit (some usec) earlier. This is covered by adding the guard
     // time `kFramePreparationGuardInterval`.
diff --git a/src/core/thread/csl_tx_scheduler.hpp b/src/core/thread/csl_tx_scheduler.hpp
index b07357d..e0378b2 100644
--- a/src/core/thread/csl_tx_scheduler.hpp
+++ b/src/core/thread/csl_tx_scheduler.hpp
@@ -97,8 +97,8 @@
         TimeMilli GetCslLastHeard(void) const { return mCslLastHeard; }
         void      SetCslLastHeard(TimeMilli aCslLastHeard) { mCslLastHeard = aCslLastHeard; }
 
-        uint64_t GetLastRxTimestamp(void) const { return mLastRxTimstamp; }
-        void     SetLastRxTimestamp(uint64_t aLastRxTimestamp) { mLastRxTimstamp = aLastRxTimestamp; }
+        uint64_t GetLastRxTimestamp(void) const { return mLastRxTimestamp; }
+        void     SetLastRxTimestamp(uint64_t aLastRxTimestamp) { mLastRxTimestamp = aLastRxTimestamp; }
 
     private:
         uint8_t   mCslTxAttempts : 7;   ///< Number of CSL triggered tx attempts.
@@ -108,7 +108,7 @@
         uint16_t  mCslPeriod;           ///< CSL sampled listening period in units of 10 symbols (160 microseconds).
         uint16_t  mCslPhase;            ///< The time when the next CSL sample will start.
         TimeMilli mCslLastHeard;        ///< Time when last frame containing CSL IE was heard.
-        uint64_t  mLastRxTimstamp;      ///< Time when last frame containing CSL IE was received, in microseconds.
+        uint64_t  mLastRxTimestamp;     ///< Time when last frame containing CSL IE was received, in microseconds.
 
         static_assert(kMaxCslTriggeredTxAttempts < (1 << 7), "mCslTxAttempts cannot fit max!");
     };
@@ -187,7 +187,7 @@
     void Clear(void);
 
 private:
-    // Guard time in usec to add when checking delay while preparaing the CSL frame for tx.
+    // Guard time in usec to add when checking delay while preparing the CSL frame for tx.
     static constexpr uint32_t kFramePreparationGuardInterval = 1500;
 
     void InitFrameRequestAhead(void);
diff --git a/src/core/thread/dua_manager.cpp b/src/core/thread/dua_manager.cpp
index d886666..53faa17 100644
--- a/src/core/thread/dua_manager.cpp
+++ b/src/core/thread/dua_manager.cpp
@@ -105,7 +105,7 @@
     switch (aState)
     {
     case BackboneRouter::Leader::kDomainPrefixUnchanged:
-        // In case removed for some reason e.g. the kDuaInvalid response from PBBR forcely
+        // In case removed for some reason e.g. the kDuaInvalid response from PBBR forcefully
         VerifyOrExit(!Get<ThreadNetif>().HasUnicastAddress(GetDomainUnicastAddress()));
 
         OT_FALL_THROUGH;
@@ -541,7 +541,7 @@
         Get<DataPollSender>().SendFastPolls();
     }
 
-    LogInfo("Sent DUA.req for DUA %s", dua.ToString().AsCString());
+    LogInfo("Sent %s for DUA %s", UriToString<kUriDuaRegistrationRequest>(), dua.ToString().AsCString());
 
 exit:
     if (error == kErrorNoBufs)
@@ -592,7 +592,7 @@
         mRegistrationTask.Post();
     }
 
-    LogInfo("Received DUA.rsp: %s", ErrorToString(error));
+    LogInfo("Received %s response: %s", UriToString<kUriDuaRegistrationRequest>(), ErrorToString(error));
 }
 
 template <>
@@ -606,14 +606,14 @@
 
     if (aMessage.IsConfirmable() && Get<Tmf::Agent>().SendEmptyAck(aMessage, aMessageInfo) == kErrorNone)
     {
-        LogInfo("Sent DUA.ntf acknowledgment");
+        LogInfo("Sent %s ack", UriToString<kUriDuaRegistrationNotify>());
     }
 
     error = ProcessDuaResponse(aMessage);
 
 exit:
     OT_UNUSED_VARIABLE(error);
-    LogInfo("Received DUA.ntf: %s", ErrorToString(error));
+    LogInfo("Received %s: %s", UriToString<kUriDuaRegistrationNotify>(), ErrorToString(error));
 }
 
 Error DuaManager::ProcessDuaResponse(Coap::Message &aMessage)
@@ -743,7 +743,8 @@
 
     SuccessOrExit(error = Get<Tmf::Agent>().SendMessage(*message, messageInfo));
 
-    LogInfo("Sent ADDR_NTF for child %04x DUA %s", aChild.GetRloc16(), aAddress.ToString().AsCString());
+    LogInfo("Sent %s for child %04x DUA %s", UriToString<kUriDuaRegistrationNotify>(), aChild.GetRloc16(),
+            aAddress.ToString().AsCString());
 
 exit:
 
@@ -752,8 +753,8 @@
         FreeMessage(message);
 
         // TODO: (DUA) (P4) may enhance to  guarantee the delivery of DUA.ntf
-        LogWarn("Sent ADDR_NTF for child %04x DUA %s Error %s", aChild.GetRloc16(), aAddress.ToString().AsCString(),
-                ErrorToString(error));
+        LogWarn("Sent %s for child %04x DUA %s Error %s", UriToString<kUriDuaRegistrationNotify>(), aChild.GetRloc16(),
+                aAddress.ToString().AsCString(), ErrorToString(error));
     }
 }
 
diff --git a/src/core/thread/energy_scan_server.cpp b/src/core/thread/energy_scan_server.cpp
index fe9a70c..c207ef9 100644
--- a/src/core/thread/energy_scan_server.cpp
+++ b/src/core/thread/energy_scan_server.cpp
@@ -102,7 +102,7 @@
     if (aMessage.IsConfirmable() && !aMessageInfo.GetSockAddr().IsMulticast())
     {
         SuccessOrExit(Get<Tmf::Agent>().SendEmptyAck(aMessage, aMessageInfo));
-        LogInfo("sent energy scan query response");
+        LogInfo("Sent %s ack", UriToString<kUriEnergyScan>());
     }
 
 exit:
@@ -197,7 +197,7 @@
 
     SuccessOrExit(error = Get<Tmf::Agent>().SendMessage(*mReportMessage, messageInfo));
 
-    LogInfo("sent scan results");
+    LogInfo("Sent %s", UriToString<kUriEnergyReport>());
 
 exit:
     FreeMessageOnError(mReportMessage, error);
diff --git a/src/core/thread/indirect_sender_frame_context.hpp b/src/core/thread/indirect_sender_frame_context.hpp
index 13d0c0d..9e14348 100644
--- a/src/core/thread/indirect_sender_frame_context.hpp
+++ b/src/core/thread/indirect_sender_frame_context.hpp
@@ -31,8 +31,8 @@
  *   This file includes definitions of frame context used for indirect transmission.
  */
 
-#ifndef INDIRECT_SENDER_FRAME_CONTETX_HPP_
-#define INDIRECT_SENDER_FRAME_CONTETX_HPP_
+#ifndef INDIRECT_SENDER_FRAME_CONTEXT_HPP_
+#define INDIRECT_SENDER_FRAME_CONTEXT_HPP_
 
 #include "openthread-core-config.h"
 
@@ -87,4 +87,4 @@
 
 } // namespace ot
 
-#endif // INDIRECT_SENDER_FRAME_CONTETX_HPP_
+#endif // INDIRECT_SENDER_FRAME_CONTEXT_HPP_
diff --git a/src/core/thread/key_manager.cpp b/src/core/thread/key_manager.cpp
index 6426889..ddcbf74 100644
--- a/src/core/thread/key_manager.cpp
+++ b/src/core/thread/key_manager.cpp
@@ -284,7 +284,7 @@
     return;
 }
 
-void KeyManager::ComputeKeys(uint32_t aKeySequence, HashKeys &aHashKeys)
+void KeyManager::ComputeKeys(uint32_t aKeySequence, HashKeys &aHashKeys) const
 {
     Crypto::HmacSha256 hmac;
     uint8_t            keySequenceBytes[sizeof(uint32_t)];
@@ -306,7 +306,7 @@
 }
 
 #if OPENTHREAD_CONFIG_RADIO_LINK_TREL_ENABLE
-void KeyManager::ComputeTrelKey(uint32_t aKeySequence, Mac::Key &aKey)
+void KeyManager::ComputeTrelKey(uint32_t aKeySequence, Mac::Key &aKey) const
 {
     Crypto::HkdfSha256 hkdf;
     uint8_t            salt[sizeof(uint32_t) + sizeof(kHkdfExtractSaltString)];
@@ -634,6 +634,15 @@
     return;
 }
 
+void KeyManager::DestroyTemporaryKeys(void)
+{
+    mMleKey.Clear();
+    mKek.Clear();
+    Get<Mac::SubMac>().ClearMacKeys();
+    Get<Mac::Mac>().ClearMode2Key();
+}
+
+void KeyManager::DestroyPersistentKeys(void) { Crypto::Storage::DestroyPersistentKeys(); }
 #endif // OPENTHREAD_CONFIG_PLATFORM_KEY_REFERENCES_ENABLE
 
 } // namespace ot
diff --git a/src/core/thread/key_manager.hpp b/src/core/thread/key_manager.hpp
index cba93e1..7428301 100644
--- a/src/core/thread/key_manager.hpp
+++ b/src/core/thread/key_manager.hpp
@@ -254,7 +254,7 @@
      * @returns A key reference to the Thread Network Key.
      *
      */
-    NetworkKeyRef GetNetworkKeyRef(void) { return mNetworkKeyRef; }
+    NetworkKeyRef GetNetworkKeyRef(void) const { return mNetworkKeyRef; }
 
     /**
      * This method sets the Thread Network Key using Key Reference.
@@ -299,7 +299,7 @@
      * @returns A key reference to the PSKc.
      *
      */
-    const PskcRef &GetPskcRef(void) { return mPskcRef; }
+    const PskcRef &GetPskcRef(void) const { return mPskcRef; }
 
     /**
      * This method sets the PSKc as a Key reference.
@@ -447,7 +447,7 @@
     void IncrementMleFrameCounter(void);
 
     /**
-     * This method returns the KEK as `KekKeyMaterail`
+     * This method returns the KEK as `KekKeyMaterial`
      *
      * @returns The KEK as `KekKeyMaterial`.
      *
@@ -545,11 +545,25 @@
      *
      * This is called to indicate the @p aMacFrameCounter value is now used.
      *
-     * @param[in]  aMacFrameCounter  The 15.4 link MAC frame counter value.
+     * @param[in]  aMacFrameCounter     The 15.4 link MAC frame counter value.
      *
      */
     void MacFrameCounterUsed(uint32_t aMacFrameCounter);
 
+#if OPENTHREAD_CONFIG_PLATFORM_KEY_REFERENCES_ENABLE
+    /**
+     * This method destroys all the volatile mac keys stored in PSA ITS.
+     *
+     */
+    void DestroyTemporaryKeys(void);
+
+    /**
+     * This method destroys all the persistent keys stored in PSA ITS.
+     *
+     */
+    void DestroyPersistentKeys(void);
+#endif
+
 private:
     static constexpr uint32_t kDefaultKeySwitchGuardTime = 624;
     static constexpr uint32_t kOneHourIntervalInMsec     = 3600u * 1000u;
@@ -571,10 +585,10 @@
         const Mac::Key &GetMacKey(void) const { return mKeys.mMacKey; }
     };
 
-    void ComputeKeys(uint32_t aKeySequence, HashKeys &aHashKeys);
+    void ComputeKeys(uint32_t aKeySequence, HashKeys &aHashKeys) const;
 
 #if OPENTHREAD_CONFIG_RADIO_LINK_TREL_ENABLE
-    void ComputeTrelKey(uint32_t aKeySequence, Mac::Key &aKey);
+    void ComputeTrelKey(uint32_t aKeySequence, Mac::Key &aKey) const;
 #endif
 
     void StartKeyRotationTimer(void);
diff --git a/src/core/thread/link_metrics.cpp b/src/core/thread/link_metrics.cpp
index 2547f1b..cfe784c 100644
--- a/src/core/thread/link_metrics.cpp
+++ b/src/core/thread/link_metrics.cpp
@@ -51,15 +51,23 @@
 
 RegisterLogModule("LinkMetrics");
 
-LinkMetrics::LinkMetrics(Instance &aInstance)
+static constexpr uint8_t kQueryIdSingleProbe = 0;   // This query ID represents Single Probe.
+static constexpr uint8_t kSeriesIdAllSeries  = 255; // This series ID represents all series.
+
+// Constants for scaling Link Margin and RSSI to raw value
+static constexpr uint8_t kMaxLinkMargin = 130;
+static constexpr int32_t kMinRssi       = -130;
+static constexpr int32_t kMaxRssi       = 0;
+
+#if OPENTHREAD_CONFIG_MLE_LINK_METRICS_INITIATOR_ENABLE
+
+Initiator::Initiator(Instance &aInstance)
     : InstanceLocator(aInstance)
 {
 }
 
-Error LinkMetrics::Query(const Ip6::Address &aDestination, uint8_t aSeriesId, const Metrics *aMetrics)
+Error Initiator::Query(const Ip6::Address &aDestination, uint8_t aSeriesId, const Metrics *aMetrics)
 {
-    static const uint8_t kTlvs[] = {Mle::Tlv::kLinkMetricsReport};
-
     Error     error;
     Neighbor *neighbor;
     QueryInfo info;
@@ -79,325 +87,43 @@
         VerifyOrExit(info.mTypeIdCount == 0, error = kErrorInvalidArgs);
     }
 
-    error = Get<Mle::MleRouter>().SendDataRequest(aDestination, kTlvs, sizeof(kTlvs), /* aDelay */ 0, info);
+    error = Get<Mle::Mle>().SendDataRequestForLinkMetricsReport(aDestination, info);
 
 exit:
     return error;
 }
 
-#if OPENTHREAD_CONFIG_MLE_LINK_METRICS_INITIATOR_ENABLE
-Error LinkMetrics::SendMgmtRequestForwardTrackingSeries(const Ip6::Address &aDestination,
-                                                        uint8_t             aSeriesId,
-                                                        const SeriesFlags  &aSeriesFlags,
-                                                        const Metrics      *aMetrics)
+Error Initiator::AppendLinkMetricsQueryTlv(Message &aMessage, const QueryInfo &aInfo)
 {
-    Error               error;
-    Neighbor           *neighbor;
-    uint8_t             typeIdCount = 0;
-    FwdProbingRegSubTlv fwdProbingSubTlv;
+    Error error = kErrorNone;
+    Tlv   tlv;
 
-    SuccessOrExit(error = FindNeighbor(aDestination, neighbor));
+    // The MLE Link Metrics Query TLV has two sub-TLVs:
+    // - Query ID sub-TLV with series ID as value.
+    // - Query Options sub-TLV with Type IDs as value.
 
-    VerifyOrExit(aSeriesId > kQueryIdSingleProbe, error = kErrorInvalidArgs);
+    tlv.SetType(Mle::Tlv::kLinkMetricsQuery);
+    tlv.SetLength(sizeof(Tlv) + sizeof(uint8_t) + ((aInfo.mTypeIdCount == 0) ? 0 : (sizeof(Tlv) + aInfo.mTypeIdCount)));
 
-    fwdProbingSubTlv.Init();
-    fwdProbingSubTlv.SetSeriesId(aSeriesId);
-    fwdProbingSubTlv.SetSeriesFlagsMask(aSeriesFlags.ConvertToMask());
-
-    if (aMetrics != nullptr)
-    {
-        typeIdCount = aMetrics->ConvertToTypeIds(fwdProbingSubTlv.GetTypeIds());
-    }
-
-    fwdProbingSubTlv.SetLength(sizeof(aSeriesId) + sizeof(uint8_t) + typeIdCount);
-
-    error = Get<Mle::MleRouter>().SendLinkMetricsManagementRequest(aDestination, fwdProbingSubTlv);
-
-exit:
-    LogDebg("SendMgmtRequestForwardTrackingSeries, error:%s, Series ID:%u", ErrorToString(error), aSeriesId);
-    return error;
-}
-
-Error LinkMetrics::SendMgmtRequestEnhAckProbing(const Ip6::Address &aDestination,
-                                                const EnhAckFlags   aEnhAckFlags,
-                                                const Metrics      *aMetrics)
-{
-    Error              error;
-    Neighbor          *neighbor;
-    uint8_t            typeIdCount = 0;
-    EnhAckConfigSubTlv enhAckConfigSubTlv;
-
-    SuccessOrExit(error = FindNeighbor(aDestination, neighbor));
-
-    if (aEnhAckFlags == kEnhAckClear)
-    {
-        VerifyOrExit(aMetrics == nullptr, error = kErrorInvalidArgs);
-    }
-
-    enhAckConfigSubTlv.Init();
-    enhAckConfigSubTlv.SetEnhAckFlags(aEnhAckFlags);
-
-    if (aMetrics != nullptr)
-    {
-        typeIdCount = aMetrics->ConvertToTypeIds(enhAckConfigSubTlv.GetTypeIds());
-    }
-
-    enhAckConfigSubTlv.SetLength(EnhAckConfigSubTlv::kMinLength + typeIdCount);
-
-    error = Get<Mle::MleRouter>().SendLinkMetricsManagementRequest(aDestination, enhAckConfigSubTlv);
-
-    if (aMetrics != nullptr)
-    {
-        neighbor->SetEnhAckProbingMetrics(*aMetrics);
-    }
-    else
-    {
-        Metrics metrics;
-
-        metrics.Clear();
-        neighbor->SetEnhAckProbingMetrics(metrics);
-    }
-
-exit:
-    return error;
-}
-
-Error LinkMetrics::SendLinkProbe(const Ip6::Address &aDestination, uint8_t aSeriesId, uint8_t aLength)
-{
-    Error     error;
-    uint8_t   buf[kLinkProbeMaxLen];
-    Neighbor *neighbor;
-
-    SuccessOrExit(error = FindNeighbor(aDestination, neighbor));
-
-    VerifyOrExit(aLength <= LinkMetrics::kLinkProbeMaxLen && aSeriesId != kQueryIdSingleProbe &&
-                     aSeriesId != kSeriesIdAllSeries,
-                 error = kErrorInvalidArgs);
-
-    error = Get<Mle::MleRouter>().SendLinkProbe(aDestination, aSeriesId, buf, aLength);
-exit:
-    LogDebg("SendLinkProbe, error:%s, Series ID:%u", ErrorToString(error), aSeriesId);
-    return error;
-}
-#endif // OPENTHREAD_CONFIG_MLE_LINK_METRICS_INITIATOR_ENABLE
-
-#if OPENTHREAD_CONFIG_MLE_LINK_METRICS_SUBJECT_ENABLE
-Error LinkMetrics::AppendReport(Message &aMessage, const Message &aRequestMessage, Neighbor &aNeighbor)
-{
-    Error         error = kErrorNone;
-    Tlv           tlv;
-    uint8_t       queryId;
-    bool          hasQueryId = false;
-    uint16_t      length;
-    uint16_t      offset;
-    uint16_t      endOffset;
-    MetricsValues values;
-
-    values.Clear();
-
-    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-    // Parse MLE Link Metrics Query TLV and its sub-TLVs from
-    // `aRequestMessage`.
-
-    SuccessOrExit(error = Tlv::FindTlvValueOffset(aRequestMessage, Mle::Tlv::Type::kLinkMetricsQuery, offset, length));
-
-    endOffset = offset + length;
-
-    while (offset < endOffset)
-    {
-        SuccessOrExit(error = aRequestMessage.Read(offset, tlv));
-
-        switch (tlv.GetType())
-        {
-        case SubTlv::kQueryId:
-            SuccessOrExit(error = Tlv::Read<QueryIdSubTlv>(aRequestMessage, offset, queryId));
-            hasQueryId = true;
-            break;
-
-        case SubTlv::kQueryOptions:
-            SuccessOrExit(error = ReadTypeIdsFromMessage(aRequestMessage, offset + sizeof(tlv),
-                                                         static_cast<uint16_t>(offset + tlv.GetSize()),
-                                                         values.GetMetrics()));
-            break;
-
-        default:
-            break;
-        }
-
-        offset += static_cast<uint16_t>(tlv.GetSize());
-    }
-
-    VerifyOrExit(hasQueryId, error = kErrorParse);
-
-    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-    // Append MLE Link Metrics Report TLV and its sub-TLVs to
-    // `aMessage`.
-
-    offset = aMessage.GetLength();
-    tlv.SetType(Mle::Tlv::kLinkMetricsReport);
     SuccessOrExit(error = aMessage.Append(tlv));
 
-    if (queryId == kQueryIdSingleProbe)
+    SuccessOrExit(error = Tlv::Append<QueryIdSubTlv>(aMessage, aInfo.mSeriesId));
+
+    if (aInfo.mTypeIdCount != 0)
     {
-        values.mPduCountValue   = aRequestMessage.GetPsduCount();
-        values.mLqiValue        = aRequestMessage.GetAverageLqi();
-        values.mLinkMarginValue = Get<Mac::Mac>().ComputeLinkMargin(aRequestMessage.GetAverageRss());
-        values.mRssiValue       = aRequestMessage.GetAverageRss();
-        SuccessOrExit(error = AppendReportSubTlvToMessage(aMessage, values));
-    }
-    else
-    {
-        SeriesInfo *seriesInfo = aNeighbor.GetForwardTrackingSeriesInfo(queryId);
+        QueryOptionsSubTlv queryOptionsTlv;
 
-        if (seriesInfo == nullptr)
-        {
-            SuccessOrExit(error = Tlv::Append<StatusSubTlv>(aMessage, kStatusSeriesIdNotRecognized));
-        }
-        else if (seriesInfo->GetPduCount() == 0)
-        {
-            SuccessOrExit(error = Tlv::Append<StatusSubTlv>(aMessage, kStatusNoMatchingFramesReceived));
-        }
-        else
-        {
-            values.SetMetrics(seriesInfo->GetLinkMetrics());
-            values.mPduCountValue   = seriesInfo->GetPduCount();
-            values.mLqiValue        = seriesInfo->GetAverageLqi();
-            values.mLinkMarginValue = Get<Mac::Mac>().ComputeLinkMargin(seriesInfo->GetAverageRss());
-            values.mRssiValue       = seriesInfo->GetAverageRss();
-            SuccessOrExit(error = AppendReportSubTlvToMessage(aMessage, values));
-        }
-    }
-
-    // Update the TLV length in message.
-    length = aMessage.GetLength() - offset - sizeof(Tlv);
-    tlv.SetLength(static_cast<uint8_t>(length));
-    aMessage.Write(offset, tlv);
-
-exit:
-    LogDebg("AppendReport, error:%s", ErrorToString(error));
-    return error;
-}
-
-Error LinkMetrics::HandleManagementRequest(const Message &aMessage, Neighbor &aNeighbor, Status &aStatus)
-{
-    Error               error = kErrorNone;
-    uint16_t            offset;
-    uint16_t            endOffset;
-    uint16_t            tlvEndOffset;
-    uint16_t            length;
-    FwdProbingRegSubTlv fwdProbingSubTlv;
-    EnhAckConfigSubTlv  enhAckConfigSubTlv;
-    Metrics             metrics;
-
-    SuccessOrExit(error = Tlv::FindTlvValueOffset(aMessage, Mle::Tlv::Type::kLinkMetricsManagement, offset, length));
-    endOffset = offset + length;
-
-    // Set sub-TLV lengths to zero to indicate that we have
-    // not yet seen them in the message.
-    fwdProbingSubTlv.SetLength(0);
-    enhAckConfigSubTlv.SetLength(0);
-
-    for (; offset < endOffset; offset = tlvEndOffset)
-    {
-        Tlv      tlv;
-        uint16_t minTlvSize;
-        Tlv     *subTlv;
-
-        SuccessOrExit(error = aMessage.Read(offset, tlv));
-
-        VerifyOrExit(offset + tlv.GetSize() <= endOffset, error = kErrorParse);
-        tlvEndOffset = static_cast<uint16_t>(offset + tlv.GetSize());
-
-        switch (tlv.GetType())
-        {
-        case SubTlv::kFwdProbingReg:
-            subTlv     = &fwdProbingSubTlv;
-            minTlvSize = sizeof(Tlv) + FwdProbingRegSubTlv::kMinLength;
-            break;
-
-        case SubTlv::kEnhAckConfig:
-            subTlv     = &enhAckConfigSubTlv;
-            minTlvSize = sizeof(Tlv) + EnhAckConfigSubTlv::kMinLength;
-            break;
-
-        default:
-            continue;
-        }
-
-        // Ensure message contains only one sub-TLV.
-        VerifyOrExit(fwdProbingSubTlv.GetLength() == 0, error = kErrorParse);
-        VerifyOrExit(enhAckConfigSubTlv.GetLength() == 0, error = kErrorParse);
-
-        VerifyOrExit(tlv.GetSize() >= minTlvSize, error = kErrorParse);
-
-        // Read `subTlv` with its `minTlvSize`, followed by the Type IDs.
-        SuccessOrExit(error = aMessage.Read(offset, subTlv, minTlvSize));
-        SuccessOrExit(error = ReadTypeIdsFromMessage(aMessage, offset + minTlvSize, tlvEndOffset, metrics));
-    }
-
-    if (fwdProbingSubTlv.GetLength() != 0)
-    {
-        aStatus = ConfigureForwardTrackingSeries(fwdProbingSubTlv.GetSeriesId(), fwdProbingSubTlv.GetSeriesFlagsMask(),
-                                                 metrics, aNeighbor);
-    }
-
-    if (enhAckConfigSubTlv.GetLength() != 0)
-    {
-        aStatus = ConfigureEnhAckProbing(enhAckConfigSubTlv.GetEnhAckFlags(), metrics, aNeighbor);
+        queryOptionsTlv.Init();
+        queryOptionsTlv.SetLength(aInfo.mTypeIdCount);
+        SuccessOrExit(error = aMessage.Append(queryOptionsTlv));
+        SuccessOrExit(error = aMessage.AppendBytes(aInfo.mTypeIds, aInfo.mTypeIdCount));
     }
 
 exit:
     return error;
 }
-#endif // OPENTHREAD_CONFIG_MLE_LINK_METRICS_SUBJECT_ENABLE
 
-Error LinkMetrics::HandleManagementResponse(const Message &aMessage, const Ip6::Address &aAddress)
-{
-    Error    error = kErrorNone;
-    uint16_t offset;
-    uint16_t endOffset;
-    uint16_t length;
-    uint8_t  status;
-    bool     hasStatus = false;
-
-    VerifyOrExit(mMgmtResponseCallback.IsSet());
-
-    SuccessOrExit(error = Tlv::FindTlvValueOffset(aMessage, Mle::Tlv::Type::kLinkMetricsManagement, offset, length));
-    endOffset = offset + length;
-
-    while (offset < endOffset)
-    {
-        Tlv tlv;
-
-        SuccessOrExit(error = aMessage.Read(offset, tlv));
-
-        switch (tlv.GetType())
-        {
-        case StatusSubTlv::kType:
-            VerifyOrExit(!hasStatus, error = kErrorParse);
-            SuccessOrExit(error = Tlv::Read<StatusSubTlv>(aMessage, offset, status));
-            hasStatus = true;
-            break;
-
-        default:
-            break;
-        }
-
-        offset += sizeof(Tlv) + tlv.GetLength();
-    }
-
-    VerifyOrExit(hasStatus, error = kErrorParse);
-
-    mMgmtResponseCallback.Invoke(&aAddress, status);
-
-exit:
-    return error;
-}
-
-void LinkMetrics::HandleReport(const Message      &aMessage,
-                               uint16_t            aOffset,
-                               uint16_t            aLength,
-                               const Ip6::Address &aAddress)
+void Initiator::HandleReport(const Message &aMessage, uint16_t aOffset, uint16_t aLength, const Ip6::Address &aAddress)
 {
     Error         error     = kErrorNone;
     uint16_t      offset    = aOffset;
@@ -499,21 +225,143 @@
     LogDebg("HandleReport, error:%s", ErrorToString(error));
 }
 
-Error LinkMetrics::HandleLinkProbe(const Message &aMessage, uint8_t &aSeriesId)
+Error Initiator::SendMgmtRequestForwardTrackingSeries(const Ip6::Address &aDestination,
+                                                      uint8_t             aSeriesId,
+                                                      const SeriesFlags  &aSeriesFlags,
+                                                      const Metrics      *aMetrics)
 {
-    Error    error = kErrorNone;
-    uint16_t offset;
-    uint16_t length;
+    Error               error;
+    Neighbor           *neighbor;
+    uint8_t             typeIdCount = 0;
+    FwdProbingRegSubTlv fwdProbingSubTlv;
 
-    SuccessOrExit(error = Tlv::FindTlvValueOffset(aMessage, Mle::Tlv::Type::kLinkProbe, offset, length));
-    VerifyOrExit(length >= sizeof(aSeriesId), error = kErrorParse);
-    error = aMessage.Read(offset, aSeriesId);
+    SuccessOrExit(error = FindNeighbor(aDestination, neighbor));
+
+    VerifyOrExit(aSeriesId > kQueryIdSingleProbe, error = kErrorInvalidArgs);
+
+    fwdProbingSubTlv.Init();
+    fwdProbingSubTlv.SetSeriesId(aSeriesId);
+    fwdProbingSubTlv.SetSeriesFlagsMask(aSeriesFlags.ConvertToMask());
+
+    if (aMetrics != nullptr)
+    {
+        typeIdCount = aMetrics->ConvertToTypeIds(fwdProbingSubTlv.GetTypeIds());
+    }
+
+    fwdProbingSubTlv.SetLength(sizeof(aSeriesId) + sizeof(uint8_t) + typeIdCount);
+
+    error = Get<Mle::Mle>().SendLinkMetricsManagementRequest(aDestination, fwdProbingSubTlv);
+
+exit:
+    LogDebg("SendMgmtRequestForwardTrackingSeries, error:%s, Series ID:%u", ErrorToString(error), aSeriesId);
+    return error;
+}
+
+Error Initiator::SendMgmtRequestEnhAckProbing(const Ip6::Address &aDestination,
+                                              EnhAckFlags         aEnhAckFlags,
+                                              const Metrics      *aMetrics)
+{
+    Error              error;
+    Neighbor          *neighbor;
+    uint8_t            typeIdCount = 0;
+    EnhAckConfigSubTlv enhAckConfigSubTlv;
+
+    SuccessOrExit(error = FindNeighbor(aDestination, neighbor));
+
+    if (aEnhAckFlags == kEnhAckClear)
+    {
+        VerifyOrExit(aMetrics == nullptr, error = kErrorInvalidArgs);
+    }
+
+    enhAckConfigSubTlv.Init();
+    enhAckConfigSubTlv.SetEnhAckFlags(aEnhAckFlags);
+
+    if (aMetrics != nullptr)
+    {
+        typeIdCount = aMetrics->ConvertToTypeIds(enhAckConfigSubTlv.GetTypeIds());
+    }
+
+    enhAckConfigSubTlv.SetLength(EnhAckConfigSubTlv::kMinLength + typeIdCount);
+
+    error = Get<Mle::Mle>().SendLinkMetricsManagementRequest(aDestination, enhAckConfigSubTlv);
+
+    if (aMetrics != nullptr)
+    {
+        neighbor->SetEnhAckProbingMetrics(*aMetrics);
+    }
+    else
+    {
+        Metrics metrics;
+
+        metrics.Clear();
+        neighbor->SetEnhAckProbingMetrics(metrics);
+    }
 
 exit:
     return error;
 }
 
-void LinkMetrics::ProcessEnhAckIeData(const uint8_t *aData, uint8_t aLength, const Neighbor &aNeighbor)
+Error Initiator::HandleManagementResponse(const Message &aMessage, const Ip6::Address &aAddress)
+{
+    Error    error = kErrorNone;
+    uint16_t offset;
+    uint16_t endOffset;
+    uint16_t length;
+    uint8_t  status;
+    bool     hasStatus = false;
+
+    VerifyOrExit(mMgmtResponseCallback.IsSet());
+
+    SuccessOrExit(error = Tlv::FindTlvValueOffset(aMessage, Mle::Tlv::Type::kLinkMetricsManagement, offset, length));
+    endOffset = offset + length;
+
+    while (offset < endOffset)
+    {
+        Tlv tlv;
+
+        SuccessOrExit(error = aMessage.Read(offset, tlv));
+
+        switch (tlv.GetType())
+        {
+        case StatusSubTlv::kType:
+            VerifyOrExit(!hasStatus, error = kErrorParse);
+            SuccessOrExit(error = Tlv::Read<StatusSubTlv>(aMessage, offset, status));
+            hasStatus = true;
+            break;
+
+        default:
+            break;
+        }
+
+        offset += sizeof(Tlv) + tlv.GetLength();
+    }
+
+    VerifyOrExit(hasStatus, error = kErrorParse);
+
+    mMgmtResponseCallback.Invoke(&aAddress, status);
+
+exit:
+    return error;
+}
+
+Error Initiator::SendLinkProbe(const Ip6::Address &aDestination, uint8_t aSeriesId, uint8_t aLength)
+{
+    Error     error;
+    uint8_t   buf[kLinkProbeMaxLen];
+    Neighbor *neighbor;
+
+    SuccessOrExit(error = FindNeighbor(aDestination, neighbor));
+
+    VerifyOrExit(aLength <= kLinkProbeMaxLen && aSeriesId != kQueryIdSingleProbe && aSeriesId != kSeriesIdAllSeries,
+                 error = kErrorInvalidArgs);
+
+    error = Get<Mle::Mle>().SendLinkProbe(aDestination, aSeriesId, buf, aLength);
+exit:
+    LogDebg("SendLinkProbe, error:%s, Series ID:%u", ErrorToString(error), aSeriesId);
+    return error;
+}
+
+void Initiator::ProcessEnhAckIeData(const uint8_t *aData, uint8_t aLength, const Neighbor &aNeighbor)
 {
     MetricsValues values;
     uint8_t       idx = 0;
@@ -541,108 +389,7 @@
     return;
 }
 
-Error LinkMetrics::AppendLinkMetricsQueryTlv(Message &aMessage, const QueryInfo &aInfo)
-{
-    Error error = kErrorNone;
-    Tlv   tlv;
-
-    // The MLE Link Metrics Query TLV has two sub-TLVs:
-    // - Query ID sub-TLV with series ID as value.
-    // - Query Options sub-TLV with Type IDs as value.
-
-    tlv.SetType(Mle::Tlv::kLinkMetricsQuery);
-    tlv.SetLength(sizeof(Tlv) + sizeof(uint8_t) + ((aInfo.mTypeIdCount == 0) ? 0 : (sizeof(Tlv) + aInfo.mTypeIdCount)));
-
-    SuccessOrExit(error = aMessage.Append(tlv));
-
-    SuccessOrExit(error = Tlv::Append<QueryIdSubTlv>(aMessage, aInfo.mSeriesId));
-
-    if (aInfo.mTypeIdCount != 0)
-    {
-        QueryOptionsSubTlv queryOptionsTlv;
-
-        queryOptionsTlv.Init();
-        queryOptionsTlv.SetLength(aInfo.mTypeIdCount);
-        SuccessOrExit(error = aMessage.Append(queryOptionsTlv));
-        SuccessOrExit(error = aMessage.AppendBytes(aInfo.mTypeIds, aInfo.mTypeIdCount));
-    }
-
-exit:
-    return error;
-}
-
-Status LinkMetrics::ConfigureForwardTrackingSeries(uint8_t        aSeriesId,
-                                                   uint8_t        aSeriesFlagsMask,
-                                                   const Metrics &aMetrics,
-                                                   Neighbor      &aNeighbor)
-{
-    Status status = kStatusSuccess;
-
-    VerifyOrExit(0 < aSeriesId, status = kStatusOtherError);
-
-    if (aSeriesFlagsMask == 0) // Remove the series
-    {
-        if (aSeriesId == kSeriesIdAllSeries) // Remove all
-        {
-            aNeighbor.RemoveAllForwardTrackingSeriesInfo();
-        }
-        else
-        {
-            SeriesInfo *seriesInfo = aNeighbor.RemoveForwardTrackingSeriesInfo(aSeriesId);
-            VerifyOrExit(seriesInfo != nullptr, status = kStatusSeriesIdNotRecognized);
-            mSeriesInfoPool.Free(*seriesInfo);
-        }
-    }
-    else // Add a new series
-    {
-        SeriesInfo *seriesInfo = aNeighbor.GetForwardTrackingSeriesInfo(aSeriesId);
-        VerifyOrExit(seriesInfo == nullptr, status = kStatusSeriesIdAlreadyRegistered);
-        seriesInfo = mSeriesInfoPool.Allocate();
-        VerifyOrExit(seriesInfo != nullptr, status = kStatusCannotSupportNewSeries);
-
-        seriesInfo->Init(aSeriesId, aSeriesFlagsMask, aMetrics);
-
-        aNeighbor.AddForwardTrackingSeriesInfo(*seriesInfo);
-    }
-
-exit:
-    return status;
-}
-
-#if OPENTHREAD_CONFIG_MLE_LINK_METRICS_SUBJECT_ENABLE
-Status LinkMetrics::ConfigureEnhAckProbing(uint8_t aEnhAckFlags, const Metrics &aMetrics, Neighbor &aNeighbor)
-{
-    Status status = kStatusSuccess;
-    Error  error  = kErrorNone;
-
-    VerifyOrExit(!aMetrics.mReserved, status = kStatusOtherError);
-
-    if (aEnhAckFlags == kEnhAckRegister)
-    {
-        VerifyOrExit(!aMetrics.mPduCount, status = kStatusOtherError);
-        VerifyOrExit(aMetrics.mLqi || aMetrics.mLinkMargin || aMetrics.mRssi, status = kStatusOtherError);
-        VerifyOrExit(!(aMetrics.mLqi && aMetrics.mLinkMargin && aMetrics.mRssi), status = kStatusOtherError);
-
-        error = Get<Radio>().ConfigureEnhAckProbing(aMetrics, aNeighbor.GetRloc16(), aNeighbor.GetExtAddress());
-    }
-    else if (aEnhAckFlags == kEnhAckClear)
-    {
-        VerifyOrExit(!aMetrics.mLqi && !aMetrics.mLinkMargin && !aMetrics.mRssi, status = kStatusOtherError);
-        error = Get<Radio>().ConfigureEnhAckProbing(aMetrics, aNeighbor.GetRloc16(), aNeighbor.GetExtAddress());
-    }
-    else
-    {
-        status = kStatusOtherError;
-    }
-
-    VerifyOrExit(error == kErrorNone, status = kStatusOtherError);
-
-exit:
-    return status;
-}
-#endif // OPENTHREAD_CONFIG_MLE_LINK_METRICS_SUBJECT_ENABLE
-
-Error LinkMetrics::FindNeighbor(const Ip6::Address &aDestination, Neighbor *&aNeighbor) const
+Error Initiator::FindNeighbor(const Ip6::Address &aDestination, Neighbor *&aNeighbor)
 {
     Error        error = kErrorUnknownNeighbor;
     Mac::Address macAddress;
@@ -662,10 +409,241 @@
     return error;
 }
 
-Error LinkMetrics::ReadTypeIdsFromMessage(const Message &aMessage,
-                                          uint16_t       aStartOffset,
-                                          uint16_t       aEndOffset,
-                                          Metrics       &aMetrics)
+#endif // OPENTHREAD_CONFIG_MLE_LINK_METRICS_INITIATOR_ENABLE
+
+#if OPENTHREAD_CONFIG_MLE_LINK_METRICS_SUBJECT_ENABLE
+Subject::Subject(Instance &aInstance)
+    : InstanceLocator(aInstance)
+{
+}
+
+Error Subject::AppendReport(Message &aMessage, const Message &aRequestMessage, Neighbor &aNeighbor)
+{
+    Error         error = kErrorNone;
+    Tlv           tlv;
+    uint8_t       queryId;
+    bool          hasQueryId = false;
+    uint16_t      length;
+    uint16_t      offset;
+    uint16_t      endOffset;
+    MetricsValues values;
+
+    values.Clear();
+
+    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Parse MLE Link Metrics Query TLV and its sub-TLVs from
+    // `aRequestMessage`.
+
+    SuccessOrExit(error = Tlv::FindTlvValueOffset(aRequestMessage, Mle::Tlv::Type::kLinkMetricsQuery, offset, length));
+
+    endOffset = offset + length;
+
+    while (offset < endOffset)
+    {
+        SuccessOrExit(error = aRequestMessage.Read(offset, tlv));
+
+        switch (tlv.GetType())
+        {
+        case SubTlv::kQueryId:
+            SuccessOrExit(error = Tlv::Read<QueryIdSubTlv>(aRequestMessage, offset, queryId));
+            hasQueryId = true;
+            break;
+
+        case SubTlv::kQueryOptions:
+            SuccessOrExit(error = ReadTypeIdsFromMessage(aRequestMessage, offset + sizeof(tlv),
+                                                         static_cast<uint16_t>(offset + tlv.GetSize()),
+                                                         values.GetMetrics()));
+            break;
+
+        default:
+            break;
+        }
+
+        offset += static_cast<uint16_t>(tlv.GetSize());
+    }
+
+    VerifyOrExit(hasQueryId, error = kErrorParse);
+
+    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Append MLE Link Metrics Report TLV and its sub-TLVs to
+    // `aMessage`.
+
+    offset = aMessage.GetLength();
+    tlv.SetType(Mle::Tlv::kLinkMetricsReport);
+    SuccessOrExit(error = aMessage.Append(tlv));
+
+    if (queryId == kQueryIdSingleProbe)
+    {
+        values.mPduCountValue   = aRequestMessage.GetPsduCount();
+        values.mLqiValue        = aRequestMessage.GetAverageLqi();
+        values.mLinkMarginValue = Get<Mac::Mac>().ComputeLinkMargin(aRequestMessage.GetAverageRss());
+        values.mRssiValue       = aRequestMessage.GetAverageRss();
+        SuccessOrExit(error = AppendReportSubTlvToMessage(aMessage, values));
+    }
+    else
+    {
+        SeriesInfo *seriesInfo = aNeighbor.GetForwardTrackingSeriesInfo(queryId);
+
+        if (seriesInfo == nullptr)
+        {
+            SuccessOrExit(error = Tlv::Append<StatusSubTlv>(aMessage, kStatusSeriesIdNotRecognized));
+        }
+        else if (seriesInfo->GetPduCount() == 0)
+        {
+            SuccessOrExit(error = Tlv::Append<StatusSubTlv>(aMessage, kStatusNoMatchingFramesReceived));
+        }
+        else
+        {
+            values.SetMetrics(seriesInfo->GetLinkMetrics());
+            values.mPduCountValue   = seriesInfo->GetPduCount();
+            values.mLqiValue        = seriesInfo->GetAverageLqi();
+            values.mLinkMarginValue = Get<Mac::Mac>().ComputeLinkMargin(seriesInfo->GetAverageRss());
+            values.mRssiValue       = seriesInfo->GetAverageRss();
+            SuccessOrExit(error = AppendReportSubTlvToMessage(aMessage, values));
+        }
+    }
+
+    // Update the TLV length in message.
+    length = aMessage.GetLength() - offset - sizeof(Tlv);
+    tlv.SetLength(static_cast<uint8_t>(length));
+    aMessage.Write(offset, tlv);
+
+exit:
+    LogDebg("AppendReport, error:%s", ErrorToString(error));
+    return error;
+}
+
+Error Subject::HandleManagementRequest(const Message &aMessage, Neighbor &aNeighbor, Status &aStatus)
+{
+    Error               error = kErrorNone;
+    uint16_t            offset;
+    uint16_t            endOffset;
+    uint16_t            tlvEndOffset;
+    uint16_t            length;
+    FwdProbingRegSubTlv fwdProbingSubTlv;
+    EnhAckConfigSubTlv  enhAckConfigSubTlv;
+    Metrics             metrics;
+
+    SuccessOrExit(error = Tlv::FindTlvValueOffset(aMessage, Mle::Tlv::Type::kLinkMetricsManagement, offset, length));
+    endOffset = offset + length;
+
+    // Set sub-TLV lengths to zero to indicate that we have
+    // not yet seen them in the message.
+    fwdProbingSubTlv.SetLength(0);
+    enhAckConfigSubTlv.SetLength(0);
+
+    for (; offset < endOffset; offset = tlvEndOffset)
+    {
+        Tlv      tlv;
+        uint16_t minTlvSize;
+        Tlv     *subTlv;
+
+        SuccessOrExit(error = aMessage.Read(offset, tlv));
+
+        VerifyOrExit(offset + tlv.GetSize() <= endOffset, error = kErrorParse);
+        tlvEndOffset = static_cast<uint16_t>(offset + tlv.GetSize());
+
+        switch (tlv.GetType())
+        {
+        case SubTlv::kFwdProbingReg:
+            subTlv     = &fwdProbingSubTlv;
+            minTlvSize = sizeof(Tlv) + FwdProbingRegSubTlv::kMinLength;
+            break;
+
+        case SubTlv::kEnhAckConfig:
+            subTlv     = &enhAckConfigSubTlv;
+            minTlvSize = sizeof(Tlv) + EnhAckConfigSubTlv::kMinLength;
+            break;
+
+        default:
+            continue;
+        }
+
+        // Ensure message contains only one sub-TLV.
+        VerifyOrExit(fwdProbingSubTlv.GetLength() == 0, error = kErrorParse);
+        VerifyOrExit(enhAckConfigSubTlv.GetLength() == 0, error = kErrorParse);
+
+        VerifyOrExit(tlv.GetSize() >= minTlvSize, error = kErrorParse);
+
+        // Read `subTlv` with its `minTlvSize`, followed by the Type IDs.
+        SuccessOrExit(error = aMessage.Read(offset, subTlv, minTlvSize));
+        SuccessOrExit(error = ReadTypeIdsFromMessage(aMessage, offset + minTlvSize, tlvEndOffset, metrics));
+    }
+
+    if (fwdProbingSubTlv.GetLength() != 0)
+    {
+        aStatus = ConfigureForwardTrackingSeries(fwdProbingSubTlv.GetSeriesId(), fwdProbingSubTlv.GetSeriesFlagsMask(),
+                                                 metrics, aNeighbor);
+    }
+
+    if (enhAckConfigSubTlv.GetLength() != 0)
+    {
+        aStatus = ConfigureEnhAckProbing(enhAckConfigSubTlv.GetEnhAckFlags(), metrics, aNeighbor);
+    }
+
+exit:
+    return error;
+}
+
+Error Subject::HandleLinkProbe(const Message &aMessage, uint8_t &aSeriesId)
+{
+    Error    error = kErrorNone;
+    uint16_t offset;
+    uint16_t length;
+
+    SuccessOrExit(error = Tlv::FindTlvValueOffset(aMessage, Mle::Tlv::Type::kLinkProbe, offset, length));
+    VerifyOrExit(length >= sizeof(aSeriesId), error = kErrorParse);
+    error = aMessage.Read(offset, aSeriesId);
+
+exit:
+    return error;
+}
+
+Error Subject::AppendReportSubTlvToMessage(Message &aMessage, const MetricsValues &aValues)
+{
+    Error        error = kErrorNone;
+    ReportSubTlv reportTlv;
+
+    reportTlv.Init();
+
+    if (aValues.mMetrics.mPduCount)
+    {
+        reportTlv.SetMetricsTypeId(TypeId::kPdu);
+        reportTlv.SetMetricsValue32(aValues.mPduCountValue);
+        SuccessOrExit(error = reportTlv.AppendTo(aMessage));
+    }
+
+    if (aValues.mMetrics.mLqi)
+    {
+        reportTlv.SetMetricsTypeId(TypeId::kLqi);
+        reportTlv.SetMetricsValue8(aValues.mLqiValue);
+        SuccessOrExit(error = reportTlv.AppendTo(aMessage));
+    }
+
+    if (aValues.mMetrics.mLinkMargin)
+    {
+        reportTlv.SetMetricsTypeId(TypeId::kLinkMargin);
+        reportTlv.SetMetricsValue8(ScaleLinkMarginToRawValue(aValues.mLinkMarginValue));
+        SuccessOrExit(error = reportTlv.AppendTo(aMessage));
+    }
+
+    if (aValues.mMetrics.mRssi)
+    {
+        reportTlv.SetMetricsTypeId(TypeId::kRssi);
+        reportTlv.SetMetricsValue8(ScaleRssiToRawValue(aValues.mRssiValue));
+        SuccessOrExit(error = reportTlv.AppendTo(aMessage));
+    }
+
+exit:
+    return error;
+}
+
+void Subject::Free(SeriesInfo &aSeriesInfo) { mSeriesInfoPool.Free(aSeriesInfo); }
+
+Error Subject::ReadTypeIdsFromMessage(const Message &aMessage,
+                                      uint16_t       aStartOffset,
+                                      uint16_t       aEndOffset,
+                                      Metrics       &aMetrics)
 {
     Error error = kErrorNone;
 
@@ -716,46 +694,78 @@
     return error;
 }
 
-Error LinkMetrics::AppendReportSubTlvToMessage(Message &aMessage, const MetricsValues &aValues)
+Status Subject::ConfigureForwardTrackingSeries(uint8_t        aSeriesId,
+                                               uint8_t        aSeriesFlagsMask,
+                                               const Metrics &aMetrics,
+                                               Neighbor      &aNeighbor)
 {
-    Error        error = kErrorNone;
-    ReportSubTlv reportTlv;
+    Status status = kStatusSuccess;
 
-    reportTlv.Init();
+    VerifyOrExit(0 < aSeriesId, status = kStatusOtherError);
 
-    if (aValues.mMetrics.mPduCount)
+    if (aSeriesFlagsMask == 0) // Remove the series
     {
-        reportTlv.SetMetricsTypeId(TypeId::kPdu);
-        reportTlv.SetMetricsValue32(aValues.mPduCountValue);
-        SuccessOrExit(error = reportTlv.AppendTo(aMessage));
+        if (aSeriesId == kSeriesIdAllSeries) // Remove all
+        {
+            aNeighbor.RemoveAllForwardTrackingSeriesInfo();
+        }
+        else
+        {
+            SeriesInfo *seriesInfo = aNeighbor.RemoveForwardTrackingSeriesInfo(aSeriesId);
+            VerifyOrExit(seriesInfo != nullptr, status = kStatusSeriesIdNotRecognized);
+            mSeriesInfoPool.Free(*seriesInfo);
+        }
     }
-
-    if (aValues.mMetrics.mLqi)
+    else // Add a new series
     {
-        reportTlv.SetMetricsTypeId(TypeId::kLqi);
-        reportTlv.SetMetricsValue8(aValues.mLqiValue);
-        SuccessOrExit(error = reportTlv.AppendTo(aMessage));
-    }
+        SeriesInfo *seriesInfo = aNeighbor.GetForwardTrackingSeriesInfo(aSeriesId);
+        VerifyOrExit(seriesInfo == nullptr, status = kStatusSeriesIdAlreadyRegistered);
+        seriesInfo = mSeriesInfoPool.Allocate();
+        VerifyOrExit(seriesInfo != nullptr, status = kStatusCannotSupportNewSeries);
 
-    if (aValues.mMetrics.mLinkMargin)
-    {
-        reportTlv.SetMetricsTypeId(TypeId::kLinkMargin);
-        reportTlv.SetMetricsValue8(ScaleLinkMarginToRawValue(aValues.mLinkMarginValue));
-        SuccessOrExit(error = reportTlv.AppendTo(aMessage));
-    }
+        seriesInfo->Init(aSeriesId, aSeriesFlagsMask, aMetrics);
 
-    if (aValues.mMetrics.mRssi)
-    {
-        reportTlv.SetMetricsTypeId(TypeId::kRssi);
-        reportTlv.SetMetricsValue8(ScaleRssiToRawValue(aValues.mRssiValue));
-        SuccessOrExit(error = reportTlv.AppendTo(aMessage));
+        aNeighbor.AddForwardTrackingSeriesInfo(*seriesInfo);
     }
 
 exit:
-    return error;
+    return status;
 }
 
-uint8_t LinkMetrics::ScaleLinkMarginToRawValue(uint8_t aLinkMargin)
+Status Subject::ConfigureEnhAckProbing(uint8_t aEnhAckFlags, const Metrics &aMetrics, Neighbor &aNeighbor)
+{
+    Status status = kStatusSuccess;
+    Error  error  = kErrorNone;
+
+    VerifyOrExit(!aMetrics.mReserved, status = kStatusOtherError);
+
+    if (aEnhAckFlags == kEnhAckRegister)
+    {
+        VerifyOrExit(!aMetrics.mPduCount, status = kStatusOtherError);
+        VerifyOrExit(aMetrics.mLqi || aMetrics.mLinkMargin || aMetrics.mRssi, status = kStatusOtherError);
+        VerifyOrExit(!(aMetrics.mLqi && aMetrics.mLinkMargin && aMetrics.mRssi), status = kStatusOtherError);
+
+        error = Get<Radio>().ConfigureEnhAckProbing(aMetrics, aNeighbor.GetRloc16(), aNeighbor.GetExtAddress());
+    }
+    else if (aEnhAckFlags == kEnhAckClear)
+    {
+        VerifyOrExit(!aMetrics.mLqi && !aMetrics.mLinkMargin && !aMetrics.mRssi, status = kStatusOtherError);
+        error = Get<Radio>().ConfigureEnhAckProbing(aMetrics, aNeighbor.GetRloc16(), aNeighbor.GetExtAddress());
+    }
+    else
+    {
+        status = kStatusOtherError;
+    }
+
+    VerifyOrExit(error == kErrorNone, status = kStatusOtherError);
+
+exit:
+    return status;
+}
+
+#endif // OPENTHREAD_CONFIG_MLE_LINK_METRICS_SUBJECT_ENABLE
+
+uint8_t ScaleLinkMarginToRawValue(uint8_t aLinkMargin)
 {
     // Linearly scale Link Margin from [0, 130] to [0, 255].
     // `kMaxLinkMargin = 130`.
@@ -769,7 +779,7 @@
     return static_cast<uint8_t>(value);
 }
 
-uint8_t LinkMetrics::ScaleRawValueToLinkMargin(uint8_t aRawValue)
+uint8_t ScaleRawValueToLinkMargin(uint8_t aRawValue)
 {
     // Scale back raw value of [0, 255] to Link Margin from [0, 130].
 
@@ -780,7 +790,7 @@
     return static_cast<uint8_t>(value);
 }
 
-uint8_t LinkMetrics::ScaleRssiToRawValue(int8_t aRssi)
+uint8_t ScaleRssiToRawValue(int8_t aRssi)
 {
     // Linearly scale RSSI from [-130, 0] to [0, 255].
     // `kMinRssi = -130`, `kMaxRssi = 0`.
@@ -794,7 +804,7 @@
     return static_cast<uint8_t>(value);
 }
 
-int8_t LinkMetrics::ScaleRawValueToRssi(uint8_t aRawValue)
+int8_t ScaleRawValueToRssi(uint8_t aRawValue)
 {
     int32_t value = aRawValue;
 
diff --git a/src/core/thread/link_metrics.hpp b/src/core/thread/link_metrics.hpp
index aef4432..0dcdd99 100644
--- a/src/core/thread/link_metrics.hpp
+++ b/src/core/thread/link_metrics.hpp
@@ -71,16 +71,18 @@
  * @{
  */
 
+#if OPENTHREAD_CONFIG_MLE_LINK_METRICS_INITIATOR_ENABLE
+
 /**
- * This class implements Thread Link Metrics query and management.
+ * This class implements the Thread Link Metrics Initiator.
+ *
+ * The Initiator makes queries, configures Link Metrics probing at the Subject and generates reports of the results.
  *
  */
-class LinkMetrics : public InstanceLocator, private NonCopyable
+class Initiator : public InstanceLocator, private NonCopyable
 {
-    friend class ot::Neighbor;
-    friend class ot::UnitTester;
-
 public:
+    // Initiator callbacks
     typedef otLinkMetricsReportCallback                ReportCallback;
     typedef otLinkMetricsMgmtResponseCallback          MgmtResponseCallback;
     typedef otLinkMetricsEnhAckProbingIeReportCallback EnhAckProbingIeReportCallback;
@@ -97,12 +99,12 @@
     };
 
     /**
-     * This constructor initializes an instance of the LinkMetrics class.
+     * This constructor initializes an instance of the Initiator class.
      *
      * @param[in]  aInstance  A reference to the OpenThread interface.
      *
      */
-    explicit LinkMetrics(Instance &aInstance);
+    explicit Initiator(Instance &aInstance);
 
     /**
      * This method sends an MLE Data Request containing Link Metrics Query TLV to query Link Metrics data.
@@ -121,7 +123,38 @@
      */
     Error Query(const Ip6::Address &aDestination, uint8_t aSeriesId, const Metrics *aMetrics);
 
-#if OPENTHREAD_CONFIG_MLE_LINK_METRICS_INITIATOR_ENABLE
+    /**
+     * This method appends MLE Link Metrics Query TLV to a given message.
+     *
+     * @param[in] aMessage     The message to append to.
+     * @param[in] aInfo        The link metrics query info to use to prepare the message.
+     *
+     * @retval kErrorNone     Successfully appended the TLV to the message.
+     * @retval kErrorNoBufs   Insufficient buffers available to append the TLV.
+     *
+     */
+    Error AppendLinkMetricsQueryTlv(Message &aMessage, const QueryInfo &aInfo);
+
+    /**
+     * This method registers a callback to handle Link Metrics report received.
+     *
+     * @param[in]  aCallback  A pointer to a function that is called when a Link Metrics report is received.
+     * @param[in]  aContext   A pointer to application-specific context.
+     *
+     */
+    void SetReportCallback(ReportCallback aCallback, void *aContext) { mReportCallback.Set(aCallback, aContext); }
+
+    /**
+     * This method handles the received Link Metrics report contained in @p aMessage.
+     *
+     * @param[in]  aMessage      A reference to the message.
+     * @param[in]  aOffset       The offset in bytes where the metrics report sub-TLVs start.
+     * @param[in]  aLength       The length of the metrics report sub-TLVs in bytes.
+     * @param[in]  aAddress      A reference to the source address of the message.
+     *
+     */
+    void HandleReport(const Message &aMessage, uint16_t aOffset, uint16_t aLength, const Ip6::Address &aAddress);
+
     /**
      * This method sends an MLE Link Metrics Management Request to configure/clear a Forward Tracking Series.
      *
@@ -142,6 +175,18 @@
                                                const Metrics      *aMetrics);
 
     /**
+     * This method registers a callback to handle Link Metrics Management Response received.
+     *
+     * @param[in]  aCallback A pointer to a function that is called when a Link Metrics Management Response is received.
+     * @param[in]  aContext  A pointer to application-specific context.
+     *
+     */
+    void SetMgmtResponseCallback(MgmtResponseCallback aCallback, void *aContext)
+    {
+        mMgmtResponseCallback.Set(aCallback, aContext);
+    }
+
+    /**
      * This method sends an MLE Link Metrics Management Request to configure/clear a Enhanced-ACK Based Probing.
      *
      * @param[in] aDestination       A reference to the IPv6 address of the destination.
@@ -161,48 +206,16 @@
                                        const Metrics      *aMetrics);
 
     /**
-     * This method sends an MLE Link Probe message.
+     * This method registers a callback to handle Link Metrics when Enh-ACK Probing IE is received.
      *
-     * @param[in] aDestination    A reference to the IPv6 address of the destination.
-     * @param[in] aSeriesId       The Series ID which the Probe message targets at.
-     * @param[in] aLength         The length of the data payload in Link Probe TLV, [0, 64].
-     *
-     * @retval kErrorNone             Successfully sent a Link Probe message.
-     * @retval kErrorNoBufs           Insufficient buffers to generate the MLE Link Probe message.
-     * @retval kErrorInvalidArgs      @p aSeriesId or @p aLength is not within the valid range.
-     * @retval kErrorUnknownNeighbor  @p aDestination is not link-local or the neighbor is not found.
+     * @param[in]  aCallback A pointer to a function that is called when Enh-ACK Probing IE is received is received.
+     * @param[in]  aContext  A pointer to application-specific context.
      *
      */
-    Error SendLinkProbe(const Ip6::Address &aDestination, uint8_t aSeriesId, uint8_t aLength);
-#endif
-
-#if OPENTHREAD_CONFIG_MLE_LINK_METRICS_SUBJECT_ENABLE
-    /**
-     * This method appends a Link Metrics Report to a message according to the Link Metrics query.
-     *
-     * @param[out]  aMessage           A reference to the message to append report.
-     * @param[in]   aRequestMessage    A reference to the message of the Data Request.
-     * @param[in]   aNeighbor          A reference to the neighbor who queries the report.
-     *
-     * @retval kErrorNone         Successfully appended the Thread Discovery TLV.
-     * @retval kErrorParse        Cannot parse query sub TLV successfully.
-     * @retval kErrorInvalidArgs  QueryId is invalid or any Type ID is invalid.
-     *
-     */
-    Error AppendReport(Message &aMessage, const Message &aRequestMessage, Neighbor &aNeighbor);
-#endif
-    /**
-     * This method handles the received Link Metrics Management Request contained in @p aMessage and return a status.
-     *
-     * @param[in]   aMessage     A reference to the message that contains the Link Metrics Management Request.
-     * @param[in]   aNeighbor    A reference to the neighbor who sends the request.
-     * @param[out]  aStatus      A reference to the status which indicates the handling result.
-     *
-     * @retval kErrorNone     Successfully handled the Link Metrics Management Request.
-     * @retval kErrorParse    Cannot parse sub-TLVs from @p aMessage successfully.
-     *
-     */
-    Error HandleManagementRequest(const Message &aMessage, Neighbor &aNeighbor, Status &aStatus);
+    void SetEnhAckProbingCallback(EnhAckProbingIeReportCallback aCallback, void *aContext)
+    {
+        mEnhAckProbingIeReportCallback.Set(aCallback, aContext);
+    }
 
     /**
      * This method handles the received Link Metrics Management Response contained in @p aMessage.
@@ -217,15 +230,89 @@
     Error HandleManagementResponse(const Message &aMessage, const Ip6::Address &aAddress);
 
     /**
-     * This method handles the received Link Metrics report contained in @p aMessage.
+     * This method sends an MLE Link Probe message.
      *
-     * @param[in]  aMessage      A reference to the message.
-     * @param[in]  aOffset       The offset in bytes where the metrics report sub-TLVs start.
-     * @param[in]  aLength       The length of the metrics report sub-TLVs in bytes.
-     * @param[in]  aAddress      A reference to the source address of the message.
+     * @param[in] aDestination    A reference to the IPv6 address of the destination.
+     * @param[in] aSeriesId       The Series ID which the Probe message targets at.
+     * @param[in] aLength         The length of the data payload in Link Probe TLV, [0, 64].
+     *
+     * @retval kErrorNone             Successfully sent a Link Probe message.
+     * @retval kErrorNoBufs           Insufficient buffers to generate the MLE Link Probe message.
+     * @retval kErrorInvalidArgs      @p aSeriesId or @p aLength is not within the valid range.
+     * @retval kErrorUnknownNeighbor  @p aDestination is not link-local or the neighbor is not found.
      *
      */
-    void HandleReport(const Message &aMessage, uint16_t aOffset, uint16_t aLength, const Ip6::Address &aAddress);
+    Error SendLinkProbe(const Ip6::Address &aDestination, uint8_t aSeriesId, uint8_t aLength);
+
+    /**
+     * This method processes received Enh-ACK Probing IE data.
+     *
+     * @param[in] aData      A pointer to buffer containing the Enh-ACK Probing IE data.
+     * @param[in] aLength    The length of @p aData.
+     * @param[in] aNeighbor  The neighbor from which the Enh-ACK Probing IE was received.
+     *
+     */
+    void ProcessEnhAckIeData(const uint8_t *aData, uint8_t aLength, const Neighbor &aNeighbor);
+
+private:
+    static constexpr uint8_t kLinkProbeMaxLen = 64; // Max length of data payload in Link Probe TLV.
+
+    Error FindNeighbor(const Ip6::Address &aDestination, Neighbor *&aNeighbor);
+
+    Callback<ReportCallback>                mReportCallback;
+    Callback<MgmtResponseCallback>          mMgmtResponseCallback;
+    Callback<EnhAckProbingIeReportCallback> mEnhAckProbingIeReportCallback;
+};
+
+#endif // OPENTHREAD_CONFIG_MLE_LINK_METRICS_INITIATOR_ENABLE
+
+#if OPENTHREAD_CONFIG_MLE_LINK_METRICS_SUBJECT_ENABLE
+
+/**
+ * This class implements the Thread Link Metrics Subject.
+ *
+ * The Subject reponds queries with reports, handles Link Metrics Management Requests and Link Probe Messages.
+ *
+ */
+class Subject : public InstanceLocator, private NonCopyable
+{
+public:
+    typedef otLinkMetricsEnhAckProbingIeReportCallback EnhAckProbingIeReportCallback;
+
+    /**
+     * This constructor initializes an instance of the Subject class.
+     *
+     * @param[in]  aInstance  A reference to the OpenThread interface.
+     *
+     */
+    explicit Subject(Instance &aInstance);
+
+    /**
+     * This method appends a Link Metrics Report to a message according to the Link Metrics query.
+     *
+     * @param[out]  aMessage           A reference to the message to append report.
+     * @param[in]   aRequestMessage    A reference to the message of the Data Request.
+     * @param[in]   aNeighbor          A reference to the neighbor who queries the report.
+     *
+     * @retval kErrorNone         Successfully appended the Thread Discovery TLV.
+     * @retval kErrorParse        Cannot parse query sub TLV successfully.
+     * @retval kErrorInvalidArgs  QueryId is invalid or any Type ID is invalid.
+     *
+     */
+    Error AppendReport(Message &aMessage, const Message &aRequestMessage, Neighbor &aNeighbor);
+
+    /**
+     * This method handles the received Link Metrics Management Request contained in @p aMessage and return a status.
+     *
+     * @param[in]   aMessage     A reference to the message that contains the Link Metrics Management Request.
+     * @param[in]   aNeighbor    A reference to the neighbor who sends the request.
+     * @param[out]  aStatus      A reference to the status which indicates the handling result.
+     *
+     * @retval kErrorNone     Successfully handled the Link Metrics Management Request.
+     * @retval kErrorParse    Cannot parse sub-TLVs from @p aMessage successfully.
+     *
+     */
+    Error HandleManagementRequest(const Message &aMessage, Neighbor &aNeighbor, Status &aStatus);
 
     /**
      * This method handles the Link Probe contained in @p aMessage.
@@ -240,100 +327,39 @@
     Error HandleLinkProbe(const Message &aMessage, uint8_t &aSeriesId);
 
     /**
-     * This method registers a callback to handle Link Metrics report received.
+     * This method frees a SeriesInfo entry that was allocated from the Subject object.
      *
-     * @param[in]  aCallback  A pointer to a function that is called when a Link Metrics report is received.
-     * @param[in]  aContext   A pointer to application-specific context.
+     * @param[in]  aSeries    A reference to the SeriesInfo to free.
      *
      */
-    void SetReportCallback(ReportCallback aCallback, void *aContext) { mReportCallback.Set(aCallback, aContext); }
-
-    /**
-     * This method registers a callback to handle Link Metrics Management Response received.
-     *
-     * @param[in]  aCallback A pointer to a function that is called when a Link Metrics Management Response is received.
-     * @param[in]  aContext  A pointer to application-specific context.
-     *
-     */
-    void SetMgmtResponseCallback(MgmtResponseCallback aCallback, void *aContext)
-    {
-        mMgmtResponseCallback.Set(aCallback, aContext);
-    }
-
-    /**
-     * This method registers a callback to handle Link Metrics when Enh-ACK Probing IE is received.
-     *
-     * @param[in]  aCallback A pointer to a function that is called when Enh-ACK Probing IE is received is received.
-     * @param[in]  aContext  A pointer to application-specific context.
-     *
-     */
-    void SetEnhAckProbingCallback(EnhAckProbingIeReportCallback aCallback, void *aContext)
-    {
-        mEnhAckProbingIeReportCallback.Set(aCallback, aContext);
-    }
-
-    /**
-     * This method processes received Enh-ACK Probing IE data.
-     *
-     * @param[in] aData      A pointer to buffer containing the Enh-ACK Probing IE data.
-     * @param[in] aLength    The length of @p aData.
-     * @param[in] aNeighbor  The neighbor from which the Enh-ACK Probing IE was received.
-     *
-     */
-    void ProcessEnhAckIeData(const uint8_t *aData, uint8_t aLength, const Neighbor &aNeighbor);
-
-    /**
-     * This method appends MLE Link Metrics Query TLV to a given message.
-     *
-     * @param[in] aMessage     The message to append to.
-     * @param[in] aInfo        The link metrics query info to use to prepare the message.
-     *
-     * @retval kErrorNone     Successfully appended the TLV to the message.
-     * @retval kErrorNoBufs   Insufficient buffers available to append the TLV.
-     *
-     */
-    Error AppendLinkMetricsQueryTlv(Message &aMessage, const QueryInfo &aInfo);
+    void Free(SeriesInfo &aSeriesInfo);
 
 private:
     // Max number of SeriesInfo that could be allocated by the pool.
     static constexpr uint16_t kMaxSeriesSupported = OPENTHREAD_CONFIG_MLE_LINK_METRICS_MAX_SERIES_SUPPORTED;
 
-    static constexpr uint8_t kQueryIdSingleProbe = 0;   // This query ID represents Single Probe.
-    static constexpr uint8_t kSeriesIdAllSeries  = 255; // This series ID represents all series.
-    static constexpr uint8_t kLinkProbeMaxLen    = 64;  // Max length of data payload in Link Probe TLV.
-
-    // Constants for scaling Link Margin and RSSI to raw value
-    static constexpr uint8_t kMaxLinkMargin = 130;
-    static constexpr int32_t kMinRssi       = -130;
-    static constexpr int32_t kMaxRssi       = 0;
-
-    Status ConfigureForwardTrackingSeries(uint8_t        aSeriesId,
-                                          uint8_t        aSeriesFlags,
-                                          const Metrics &aMetrics,
-                                          Neighbor      &aNeighbor);
-
-    Status ConfigureEnhAckProbing(uint8_t aEnhAckFlags, const Metrics &aMetrics, Neighbor &aNeighbor);
-
-    Error FindNeighbor(const Ip6::Address &aDestination, Neighbor *&aNeighbor) const;
-
     static Error ReadTypeIdsFromMessage(const Message &aMessage,
                                         uint16_t       aStartOffset,
                                         uint16_t       aEndOffset,
                                         Metrics       &aMetrics);
     static Error AppendReportSubTlvToMessage(Message &aMessage, const MetricsValues &aValues);
 
-    static uint8_t ScaleLinkMarginToRawValue(uint8_t aLinkMargin);
-    static uint8_t ScaleRawValueToLinkMargin(uint8_t aRawValue);
-    static uint8_t ScaleRssiToRawValue(int8_t aRssi);
-    static int8_t  ScaleRawValueToRssi(uint8_t aRawValue);
-
-    Callback<ReportCallback>                mReportCallback;
-    Callback<MgmtResponseCallback>          mMgmtResponseCallback;
-    Callback<EnhAckProbingIeReportCallback> mEnhAckProbingIeReportCallback;
+    Status ConfigureForwardTrackingSeries(uint8_t        aSeriesId,
+                                          uint8_t        aSeriesFlags,
+                                          const Metrics &aMetrics,
+                                          Neighbor      &aNeighbor);
+    Status ConfigureEnhAckProbing(uint8_t aEnhAckFlags, const Metrics &aMetrics, Neighbor &aNeighbor);
 
     Pool<SeriesInfo, kMaxSeriesSupported> mSeriesInfoPool;
 };
 
+#endif // OPENTHREAD_CONFIG_MLE_LINK_METRICS_SUBJECT_ENABLE
+
+uint8_t ScaleLinkMarginToRawValue(uint8_t aLinkMargin);
+uint8_t ScaleRawValueToLinkMargin(uint8_t aRawValue);
+uint8_t ScaleRssiToRawValue(int8_t aRssi);
+int8_t  ScaleRawValueToRssi(uint8_t aRawValue);
+
 /**
  * @}
  */
diff --git a/src/core/thread/link_metrics_types.cpp b/src/core/thread/link_metrics_types.cpp
index cd58721..8a5fece 100644
--- a/src/core/thread/link_metrics_types.cpp
+++ b/src/core/thread/link_metrics_types.cpp
@@ -73,7 +73,7 @@
     {
         for (uint8_t i = 0; i < count; i++)
         {
-            TypeId::MarkAsReserverd(aTypeIds[i]);
+            TypeId::MarkAsReserved(aTypeIds[i]);
         }
     }
 #endif
diff --git a/src/core/thread/link_metrics_types.hpp b/src/core/thread/link_metrics_types.hpp
index f0d564a..eb10b2c 100644
--- a/src/core/thread/link_metrics_types.hpp
+++ b/src/core/thread/link_metrics_types.hpp
@@ -173,7 +173,7 @@
      * @param[in, out] aTypeId    A reference to a Type ID variable to update.
      *
      */
-    static void MarkAsReserverd(uint8_t &aTypeId) { aTypeId = (aTypeId & ~kTypeMask) | kTypeReserved; }
+    static void MarkAsReserved(uint8_t &aTypeId) { aTypeId = (aTypeId & ~kTypeMask) | kTypeReserved; }
 
     TypeId(void) = delete;
 };
diff --git a/src/core/thread/link_quality.hpp b/src/core/thread/link_quality.hpp
index 41932d8..9c4bc4c 100644
--- a/src/core/thread/link_quality.hpp
+++ b/src/core/thread/link_quality.hpp
@@ -250,7 +250,7 @@
 /**
  * This function converts link quality to route cost.
  *
- * @param[in]  aLinkQuality  The link quality to covert.
+ * @param[in]  aLinkQuality  The link quality to convert.
  *
  * @returns The route cost corresponding to @p aLinkQuality.
  *
@@ -326,7 +326,7 @@
      * This method clears the average RSS value.
      *
      */
-    void CleaAverageRss(void) { mRssAverager.Clear(); }
+    void ClearAverageRss(void) { mRssAverager.Clear(); }
 
     /**
      * This method adds a new received signal strength (RSS) value to the average.
diff --git a/src/core/thread/lowpan.hpp b/src/core/thread/lowpan.hpp
index e9853c2..77139a6 100644
--- a/src/core/thread/lowpan.hpp
+++ b/src/core/thread/lowpan.hpp
@@ -165,7 +165,7 @@
      * @param[in]     aMacAddrs              The MAC source and destination addresses
      * @param[in,out] aFrameData             A frame data containing the LOWPAN_IPHC header.
      *
-     * @retval kErrorNone    The header was decompressed successfully. @p aIp6Headre and @p aFrameData are updated.
+     * @retval kErrorNone    The header was decompressed successfully. @p aIp6Header and @p aFrameData are updated.
      * @retval kErrorParse   Failed to parse the lowpan header.
      *
      */
@@ -418,9 +418,9 @@
     /**
      * This method appends the Mesh Header into a given frame.
      *
-     * @param[out]  aFrameBuilder  The `FrameBuidler` to append to.
+     * @param[out]  aFrameBuilder  The `FrameBuilder` to append to.
      *
-     * @retval kErrorNone    Successfully appended the MeshHeader to @p aFrameBuildr.
+     * @retval kErrorNone    Successfully appended the MeshHeader to @p aFrameBuilder.
      * @retval kErrorNoBufs  Insufficient available buffers.
      *
      */
diff --git a/src/core/thread/mesh_forwarder.cpp b/src/core/thread/mesh_forwarder.cpp
index 9af353b..a88830c 100644
--- a/src/core/thread/mesh_forwarder.cpp
+++ b/src/core/thread/mesh_forwarder.cpp
@@ -1496,15 +1496,15 @@
 
 void MeshForwarder::HandleTimeTick(void)
 {
-    bool contineRxingTicks = false;
+    bool continueRxingTicks = false;
 
 #if OPENTHREAD_FTD
-    contineRxingTicks = mFragmentPriorityList.UpdateOnTimeTick();
+    continueRxingTicks = mFragmentPriorityList.UpdateOnTimeTick();
 #endif
 
-    contineRxingTicks = UpdateReassemblyList() || contineRxingTicks;
+    continueRxingTicks = UpdateReassemblyList() || continueRxingTicks;
 
-    if (!contineRxingTicks)
+    if (!continueRxingTicks)
     {
         Get<TimeTicker>().UnregisterReceiver(TimeTicker::kMeshForwarder);
     }
@@ -1626,10 +1626,14 @@
     {
         uint16_t destPort = headers.GetUdpHeader().GetDestinationPort();
 
-        if ((destPort == Mle::kUdpPort) || (destPort == Tmf::kUdpPort))
+        if (destPort == Mle::kUdpPort)
         {
             aPriority = Message::kPriorityNet;
         }
+        else if (Get<Tmf::Agent>().IsTmfMessage(headers.GetSourceAddress(), headers.GetDestinationAddress(), destPort))
+        {
+            aPriority = Tmf::Agent::DscpToPriority(headers.GetIp6Header().GetDscp());
+        }
     }
 
 exit:
diff --git a/src/core/thread/mesh_forwarder_ftd.cpp b/src/core/thread/mesh_forwarder_ftd.cpp
index 2df2775..ffec4c7 100644
--- a/src/core/thread/mesh_forwarder_ftd.cpp
+++ b/src/core/thread/mesh_forwarder_ftd.cpp
@@ -769,7 +769,7 @@
 
 bool MeshForwarder::FragmentPriorityList::UpdateOnTimeTick(void)
 {
-    bool contineRxingTicks = false;
+    bool continueRxingTicks = false;
 
     for (Entry &entry : mEntries)
     {
@@ -779,12 +779,12 @@
 
             if (!entry.IsExpired())
             {
-                contineRxingTicks = true;
+                continueRxingTicks = true;
             }
         }
     }
 
-    return contineRxingTicks;
+    return continueRxingTicks;
 }
 
 void MeshForwarder::UpdateFragmentPriority(Lowpan::FragmentHeader &aFragmentHeader,
diff --git a/src/core/thread/mle.cpp b/src/core/thread/mle.cpp
index 5354c6c..bd65101 100644
--- a/src/core/thread/mle.cpp
+++ b/src/core/thread/mle.cpp
@@ -73,6 +73,7 @@
 Mle::Mle(Instance &aInstance)
     : InstanceLocator(aInstance)
     , mRetrieveNewNetworkData(false)
+    , mRequestRouteTlv(false)
     , mRole(kRoleDisabled)
     , mNeighborTable(aInstance)
     , mDeviceMode(DeviceMode::kModeRxOnWhenIdle)
@@ -259,6 +260,14 @@
     }
 }
 
+void Mle::ResetCounters(void)
+{
+    memset(&mCounters, 0, sizeof(mCounters));
+#if OPENTHREAD_CONFIG_UPTIME_ENABLE
+    mLastUpdatedTimestamp = Get<Uptime>().GetUptime();
+#endif
+}
+
 #if OPENTHREAD_CONFIG_UPTIME_ENABLE
 void Mle::UpdateRoleTimeCounters(DeviceRole aRole)
 {
@@ -1387,7 +1396,7 @@
     switch (mAttachState)
     {
     case kAttachStateAnnounce:
-        VerifyOrExit(!HasMoreChannelsToAnnouce());
+        VerifyOrExit(!HasMoreChannelsToAnnounce());
         break;
 
     case kAttachStateParentRequest:
@@ -1495,7 +1504,7 @@
         OT_FALL_THROUGH;
 
     case kAttachStateAnnounce:
-        if (shouldAnnounce && (GetNextAnnouceChannel(mAnnounceChannel) == kErrorNone))
+        if (shouldAnnounce && (GetNextAnnounceChannel(mAnnounceChannel) == kErrorNone))
         {
             SendAnnounce(mAnnounceChannel, kOrphanAnnounce);
             delay = mAnnounceDelay;
@@ -1833,12 +1842,35 @@
     return error;
 }
 
-#if OPENTHREAD_CONFIG_MLE_LINK_METRICS_INITIATOR_ENABLE || OPENTHREAD_CONFIG_MLE_LINK_METRICS_SUBJECT_ENABLE
-Error Mle::SendDataRequest(const Ip6::Address                        &aDestination,
-                           const uint8_t                             *aTlvs,
-                           uint8_t                                    aTlvsLength,
-                           uint16_t                                   aDelay,
-                           const LinkMetrics::LinkMetrics::QueryInfo *aQueryInfo)
+Error Mle::SendDataRequest(const Ip6::Address &aDestination)
+{
+    return SendDataRequestAfterDelay(aDestination, /* aDelay */ 0);
+}
+
+Error Mle::SendDataRequestAfterDelay(const Ip6::Address &aDestination, uint16_t aDelay)
+{
+    static const uint8_t kTlvs[] = {Tlv::kNetworkData, Tlv::kRoute};
+
+    // Based on `mRequestRouteTlv` include both Network Data and Route
+    // TLVs or only Network Data TLV.
+
+    return SendDataRequest(aDestination, kTlvs, mRequestRouteTlv ? 2 : 1, aDelay);
+}
+
+#if OPENTHREAD_CONFIG_MLE_LINK_METRICS_INITIATOR_ENABLE
+Error Mle::SendDataRequestForLinkMetricsReport(const Ip6::Address                      &aDestination,
+                                               const LinkMetrics::Initiator::QueryInfo &aQueryInfo)
+{
+    static const uint8_t kTlvs[] = {Tlv::kLinkMetricsReport};
+
+    return SendDataRequest(aDestination, kTlvs, sizeof(kTlvs), /* aDelay */ 0, &aQueryInfo);
+}
+
+Error Mle::SendDataRequest(const Ip6::Address                      &aDestination,
+                           const uint8_t                           *aTlvs,
+                           uint8_t                                  aTlvsLength,
+                           uint16_t                                 aDelay,
+                           const LinkMetrics::Initiator::QueryInfo *aQueryInfo)
 #else
 Error Mle::SendDataRequest(const Ip6::Address &aDestination, const uint8_t *aTlvs, uint8_t aTlvsLength, uint16_t aDelay)
 #endif
@@ -1851,10 +1883,10 @@
     VerifyOrExit((message = NewMleMessage(kCommandDataRequest)) != nullptr, error = kErrorNoBufs);
     SuccessOrExit(error = message->AppendTlvRequestTlv(aTlvs, aTlvsLength));
 
-#if OPENTHREAD_CONFIG_MLE_LINK_METRICS_INITIATOR_ENABLE || OPENTHREAD_CONFIG_MLE_LINK_METRICS_SUBJECT_ENABLE
+#if OPENTHREAD_CONFIG_MLE_LINK_METRICS_INITIATOR_ENABLE
     if (aQueryInfo != nullptr)
     {
-        SuccessOrExit(error = Get<LinkMetrics::LinkMetrics>().AppendLinkMetricsQueryTlv(*message, *aQueryInfo));
+        SuccessOrExit(error = Get<LinkMetrics::Initiator>().AppendLinkMetricsQueryTlv(*message, *aQueryInfo));
     }
 #endif
 
@@ -1979,15 +2011,13 @@
     case kChildUpdateRequestNone:
         if (mDataRequestState == kDataRequestActive)
         {
-            static const uint8_t kTlvs[] = {Tlv::kNetworkData};
-
             Ip6::Address destination;
 
             VerifyOrExit(mDataRequestAttempts < kMaxChildKeepAliveAttempts, IgnoreError(BecomeDetached()));
 
             destination.SetToLinkLocalAddress(mParent.GetExtAddress());
 
-            if (SendDataRequest(destination, kTlvs) == kErrorNone)
+            if (SendDataRequest(destination) == kErrorNone)
             {
                 mDataRequestAttempts++;
             }
@@ -2027,14 +2057,14 @@
     return;
 }
 
-Error Mle::SendChildUpdateRequest(bool aAppendChallenge) { return SendChildUpdateRequest(aAppendChallenge, mTimeout); }
+Error Mle::SendChildUpdateRequest(void) { return SendChildUpdateRequest(kNormalChildUpdateRequest); }
 
-Error Mle::SendChildUpdateRequest(bool aAppendChallenge, uint32_t aTimeout)
+Error Mle::SendChildUpdateRequest(ChildUpdateRequestMode aMode)
 {
     Error                   error = kErrorNone;
     Ip6::Address            destination;
-    TxMessage              *message = nullptr;
-    AddressRegistrationMode mode    = kAppendAllAddresses;
+    TxMessage              *message     = nullptr;
+    AddressRegistrationMode addrRegMode = kAppendAllAddresses;
 
     if (!mParent.IsStateValidOrRestoring())
     {
@@ -2049,7 +2079,7 @@
     VerifyOrExit((message = NewMleMessage(kCommandChildUpdateRequest)) != nullptr, error = kErrorNoBufs);
     SuccessOrExit(error = message->AppendModeTlv(mDeviceMode));
 
-    if (aAppendChallenge || IsDetached())
+    if ((aMode == kAppendChallengeTlv) || IsDetached())
     {
         mParentRequestChallenge.GenerateRandom();
         SuccessOrExit(error = message->AppendChallengeTlv(mParentRequestChallenge));
@@ -2058,13 +2088,13 @@
     switch (mRole)
     {
     case kRoleDetached:
-        mode = kAppendMeshLocalOnly;
+        addrRegMode = kAppendMeshLocalOnly;
         break;
 
     case kRoleChild:
         SuccessOrExit(error = message->AppendSourceAddressTlv());
         SuccessOrExit(error = message->AppendLeaderDataTlv());
-        SuccessOrExit(error = message->AppendTimeoutTlv(aTimeout));
+        SuccessOrExit(error = message->AppendTimeoutTlv((aMode == kAppendZeroTimeout) ? 0 : mTimeout));
         SuccessOrExit(error = message->AppendSupervisionIntervalTlv(Get<SupervisionListener>().GetInterval()));
 #if OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_ENABLE
         if (Get<Mac::Mac>().IsCslEnabled())
@@ -2083,7 +2113,7 @@
 
     if (!IsFullThreadDevice())
     {
-        SuccessOrExit(error = message->AppendAddressRegistrationTlv(mode));
+        SuccessOrExit(error = message->AppendAddressRegistrationTlv(addrRegMode));
     }
 
     destination.SetToLinkLocalAddress(mParent.GetExtAddress());
@@ -2238,7 +2268,7 @@
     FreeMessageOnError(message, error);
 }
 
-Error Mle::GetNextAnnouceChannel(uint8_t &aChannel) const
+Error Mle::GetNextAnnounceChannel(uint8_t &aChannel) const
 {
     // This method gets the next channel to send announce on after
     // `aChannel`. Returns `kErrorNotFound` if no more channel in the
@@ -2254,11 +2284,11 @@
     return channelMask.GetNextChannel(aChannel);
 }
 
-bool Mle::HasMoreChannelsToAnnouce(void) const
+bool Mle::HasMoreChannelsToAnnounce(void) const
 {
     uint8_t channel = mAnnounceChannel;
 
-    return GetNextAnnouceChannel(channel) == kErrorNone;
+    return GetNextAnnounceChannel(channel) == kErrorNone;
 }
 
 #if OPENTHREAD_CONFIG_MLE_LINK_METRICS_SUBJECT_ENABLE
@@ -2707,7 +2737,7 @@
 
     if (IsChild() && (&aNeighbor == &mParent))
     {
-        IgnoreError(SendChildUpdateRequest(/* aAppendChallenge */ true));
+        IgnoreError(SendChildUpdateRequest(kAppendChallengeTlv));
         ExitNow();
     }
 
@@ -2733,8 +2763,6 @@
 
 void Mle::HandleAdvertisement(RxInfo &aRxInfo)
 {
-    static const uint8_t kTlvs[] = {Tlv::kNetworkData};
-
     Error      error = kErrorNone;
     uint16_t   sourceAddress;
     LeaderData leaderData;
@@ -2788,7 +2816,7 @@
     if (mRetrieveNewNetworkData || IsNetworkDataNewer(leaderData))
     {
         delay = Random::NonCrypto::GetUint16InRange(0, kMleMaxResponseDelay);
-        IgnoreError(SendDataRequest(aRxInfo.mMessageInfo.GetPeerAddr(), kTlvs, delay));
+        IgnoreError(SendDataRequestAfterDelay(aRxInfo.mMessageInfo.GetPeerAddr(), delay));
     }
 
     aRxInfo.mClass = RxInfo::kPeerMessage;
@@ -2812,12 +2840,16 @@
 
         if (Tlv::FindTlvValueOffset(aRxInfo.mMessage, Tlv::kLinkMetricsReport, offset, length) == kErrorNone)
         {
-            Get<LinkMetrics::LinkMetrics>().HandleReport(aRxInfo.mMessage, offset, length,
-                                                         aRxInfo.mMessageInfo.GetPeerAddr());
+            Get<LinkMetrics::Initiator>().HandleReport(aRxInfo.mMessage, offset, length,
+                                                       aRxInfo.mMessageInfo.GetPeerAddr());
         }
     }
 #endif
 
+#if OPENTHREAD_FTD
+    SuccessOrExit(error = Get<MleRouter>().ReadAndProcessRouteTlvOnFed(aRxInfo, mParent.GetRouterId()));
+#endif
+
     error = HandleLeaderData(aRxInfo);
 
     if (mDataRequestState == kDataRequestNone && !IsRxOnWhenIdle())
@@ -2981,8 +3013,6 @@
 
     if (dataRequest)
     {
-        static const uint8_t kTlvs[] = {Tlv::kNetworkData};
-
         uint16_t delay;
 
         if (aRxInfo.mMessageInfo.GetSockAddr().IsMulticast())
@@ -2997,7 +3027,7 @@
             delay = 10;
         }
 
-        IgnoreError(SendDataRequest(aRxInfo.mMessageInfo.GetPeerAddr(), kTlvs, delay));
+        IgnoreError(SendDataRequestAfterDelay(aRxInfo.mMessageInfo.GetPeerAddr(), delay));
     }
     else if (error == kErrorNone)
     {
@@ -3153,7 +3183,7 @@
     cslAccuracy.Init();
 #endif
 
-    // Share data with application, if requested.
+#if OPENTHREAD_CONFIG_MLE_PARENT_RESPONSE_CALLBACK_API_ENABLE
     if (mParentResponseCallback.IsSet())
     {
         otThreadParentResponseInfo parentinfo;
@@ -3169,6 +3199,7 @@
 
         mParentResponseCallback.Invoke(&parentinfo);
     }
+#endif
 
     aRxInfo.mClass = RxInfo::kAuthoritativeMessage;
 
@@ -3575,6 +3606,13 @@
 
         mRetrieveNewNetworkData = true;
 
+#if OPENTHREAD_FTD
+        if (IsFullThreadDevice())
+        {
+            mRequestRouteTlv = true;
+        }
+#endif
+
         OT_FALL_THROUGH;
 
     case kRoleChild:
@@ -3741,7 +3779,7 @@
     VerifyOrExit(aRxInfo.mNeighbor != nullptr, error = kErrorInvalidState);
 
     SuccessOrExit(
-        error = Get<LinkMetrics::LinkMetrics>().HandleManagementRequest(aRxInfo.mMessage, *aRxInfo.mNeighbor, status));
+        error = Get<LinkMetrics::Subject>().HandleManagementRequest(aRxInfo.mMessage, *aRxInfo.mNeighbor, status));
 
     error = SendLinkMetricsManagementResponse(aRxInfo.mMessageInfo.GetPeerAddr(), status);
 
@@ -3763,7 +3801,7 @@
     VerifyOrExit(aRxInfo.mNeighbor != nullptr, error = kErrorInvalidState);
 
     error =
-        Get<LinkMetrics::LinkMetrics>().HandleManagementResponse(aRxInfo.mMessage, aRxInfo.mMessageInfo.GetPeerAddr());
+        Get<LinkMetrics::Initiator>().HandleManagementResponse(aRxInfo.mMessage, aRxInfo.mMessageInfo.GetPeerAddr());
 
     aRxInfo.mClass = RxInfo::kPeerMessage;
 
@@ -3780,7 +3818,7 @@
 
     Log(kMessageReceive, kTypeLinkProbe, aRxInfo.mMessageInfo.GetPeerAddr());
 
-    SuccessOrExit(error = Get<LinkMetrics::LinkMetrics>().HandleLinkProbe(aRxInfo.mMessage, seriesId));
+    SuccessOrExit(error = Get<LinkMetrics::Subject>().HandleLinkProbe(aRxInfo.mMessage, seriesId));
     aRxInfo.mNeighbor->AggregateLinkMetrics(seriesId, LinkMetrics::SeriesInfo::kSeriesTypeLinkProbe,
                                             aRxInfo.mMessage.GetAverageLqi(), aRxInfo.mMessage.GetAverageRss());
 
@@ -3925,9 +3963,9 @@
     LogInfo("PeriodicParentSearch: Parent RSS %d", parentRss);
     VerifyOrExit(parentRss != Radio::kInvalidRssi);
 
-    if (parentRss < kRssThreadhold)
+    if (parentRss < kRssThreshold)
     {
-        LogInfo("PeriodicParentSearch: Parent RSS less than %d, searching for new parents", kRssThreadhold);
+        LogInfo("PeriodicParentSearch: Parent RSS less than %d, searching for new parents", kRssThreshold);
         mIsInBackoff = true;
         Get<Mle>().Attach(kBetterParent);
     }
@@ -4330,7 +4368,7 @@
         break;
 
     case kRoleChild:
-        IgnoreError(SendChildUpdateRequest(/* aAppendChallenge */ false, /* aTimeout */ 0));
+        IgnoreError(SendChildUpdateRequest(kAppendZeroTimeout));
         break;
 
     case kRoleDisabled:
@@ -4577,22 +4615,17 @@
 
 Error Mle::TxMessage::AppendAddressRegistrationTlv(AddressRegistrationMode aMode)
 {
-    Error                    error = kErrorNone;
-    Tlv                      tlv;
-    AddressRegistrationEntry entry;
-    Lowpan::Context          context;
-    uint8_t                  length      = 0;
-    uint8_t                  counter     = 0;
-    uint16_t                 startOffset = GetLength();
+    Error           error = kErrorNone;
+    Tlv             tlv;
+    Lowpan::Context context;
+    uint8_t         counter     = 0;
+    uint16_t        startOffset = GetLength();
 
     tlv.SetType(Tlv::kAddressRegistration);
     SuccessOrExit(error = Append(tlv));
 
     // Prioritize ML-EID
-    entry.SetContextId(kMeshLocalPrefixContextId);
-    entry.SetIid(Get<Mle>().GetMeshLocal64().GetIid());
-    SuccessOrExit(error = AppendBytes(&entry, entry.GetLength()));
-    length += entry.GetLength();
+    SuccessOrExit(error = AppendCompressedAddressEntry(kMeshLocalPrefixContextId, Get<Mle>().GetMeshLocal64()));
 
     // Continue to append the other addresses if not `kAppendMeshLocalOnly` mode
     VerifyOrExit(aMode != kAppendMeshLocalOnly);
@@ -4603,10 +4636,8 @@
         (Get<NetworkData::Leader>().GetContext(Get<DuaManager>().GetDomainUnicastAddress(), context) == kErrorNone))
     {
         // Prioritize DUA, compressed entry
-        entry.SetContextId(context.mContextId);
-        entry.SetIid(Get<DuaManager>().GetDomainUnicastAddress().GetIid());
-        SuccessOrExit(error = AppendBytes(&entry, entry.GetLength()));
-        length += entry.GetLength();
+        SuccessOrExit(
+            error = AppendCompressedAddressEntry(context.mContextId, Get<DuaManager>().GetDomainUnicastAddress()));
         counter++;
     }
 #endif
@@ -4628,19 +4659,13 @@
 
         if (Get<NetworkData::Leader>().GetContext(addr.GetAddress(), context) == kErrorNone)
         {
-            // compressed entry
-            entry.SetContextId(context.mContextId);
-            entry.SetIid(addr.GetAddress().GetIid());
+            SuccessOrExit(error = AppendCompressedAddressEntry(context.mContextId, addr.GetAddress()));
         }
         else
         {
-            // uncompressed entry
-            entry.SetUncompressed();
-            entry.SetIp6Address(addr.GetAddress());
+            SuccessOrExit(error = AppendAddressEntry(addr.GetAddress()));
         }
 
-        SuccessOrExit(error = AppendBytes(&entry, entry.GetLength()));
-        length += entry.GetLength();
         counter++;
         // only continue to append if there is available entry.
         VerifyOrExit(counter < kMaxIpAddressesToRegister);
@@ -4668,11 +4693,7 @@
             }
 #endif
 
-            entry.SetUncompressed();
-            entry.SetIp6Address(addr.GetAddress());
-            SuccessOrExit(error = AppendBytes(&entry, entry.GetLength()));
-            length += entry.GetLength();
-
+            SuccessOrExit(error = AppendAddressEntry(addr.GetAddress()));
             counter++;
             // only continue to append if there is available entry.
             VerifyOrExit(counter < kMaxIpAddressesToRegister);
@@ -4681,15 +4702,44 @@
 
 exit:
 
-    if (error == kErrorNone && length > 0)
+    if (error == kErrorNone)
     {
-        tlv.SetLength(length);
+        tlv.SetLength(static_cast<uint8_t>(GetLength() - startOffset - sizeof(Tlv)));
         Write(startOffset, tlv);
     }
 
     return error;
 }
 
+Error Mle::TxMessage::AppendCompressedAddressEntry(uint8_t aContextId, const Ip6::Address &aAddress)
+{
+    // Append an IPv6 address entry in an Address Registration TLV
+    // using compressed format (context ID with IID).
+
+    Error error;
+
+    SuccessOrExit(error = Append<uint8_t>(AddressRegistrationTlv::ControlByteFor(aContextId)));
+    error = Append(aAddress.GetIid());
+
+exit:
+    return error;
+}
+
+Error Mle::TxMessage::AppendAddressEntry(const Ip6::Address &aAddress)
+{
+    // Append an IPv6 address entry in an Address Registration TLV
+    // using uncompressed format
+
+    Error   error;
+    uint8_t controlByte = AddressRegistrationTlv::kControlByteUncompressed;
+
+    SuccessOrExit(error = Append(controlByte));
+    error = Append(aAddress);
+
+exit:
+    return error;
+}
+
 Error Mle::TxMessage::AppendSupervisionIntervalTlv(uint16_t aInterval)
 {
     return Tlv::Append<SupervisionIntervalTlv>(*this, aInterval);
@@ -4852,14 +4902,12 @@
     return tlv.AppendTo(*this);
 }
 
-Error Mle::TxMessage::AppendAddresseRegisterationTlv(Child &aChild)
+Error Mle::TxMessage::AppendAddressRegistrationTlv(Child &aChild)
 {
-    Error                    error;
-    Tlv                      tlv;
-    AddressRegistrationEntry entry;
-    Lowpan::Context          context;
-    uint8_t                  length      = 0;
-    uint16_t                 startOffset = GetLength();
+    Error           error;
+    Tlv             tlv;
+    Lowpan::Context context;
+    uint16_t        startOffset = GetLength();
 
     tlv.SetType(Tlv::kAddressRegistration);
     SuccessOrExit(error = Append(tlv));
@@ -4868,26 +4916,19 @@
     {
         if (address.IsMulticast() || Get<NetworkData::Leader>().GetContext(address, context) != kErrorNone)
         {
-            // uncompressed entry
-            entry.SetUncompressed();
-            entry.SetIp6Address(address);
+            SuccessOrExit(error = AppendAddressEntry(address));
         }
         else if (context.mContextId != kMeshLocalPrefixContextId)
         {
-            // compressed entry
-            entry.SetContextId(context.mContextId);
-            entry.SetIid(address.GetIid());
+            SuccessOrExit(error = AppendCompressedAddressEntry(context.mContextId, address));
         }
         else
         {
             continue;
         }
-
-        SuccessOrExit(error = AppendBytes(&entry, entry.GetLength()));
-        length += entry.GetLength();
     }
 
-    tlv.SetLength(length);
+    tlv.SetLength(static_cast<uint8_t>(GetLength() - startOffset - sizeof(Tlv)));
     Write(startOffset, tlv);
 
 exit:
diff --git a/src/core/thread/mle.hpp b/src/core/thread/mle.hpp
index c73c4e8..f33b525 100644
--- a/src/core/thread/mle.hpp
+++ b/src/core/thread/mle.hpp
@@ -103,8 +103,8 @@
     friend class DiscoverScanner;
     friend class ot::Notifier;
     friend class ot::SupervisionListener;
-#if OPENTHREAD_CONFIG_MLE_LINK_METRICS_INITIATOR_ENABLE || OPENTHREAD_CONFIG_MLE_LINK_METRICS_SUBJECT_ENABLE
-    friend class ot::LinkMetrics::LinkMetrics;
+#if OPENTHREAD_CONFIG_MLE_LINK_METRICS_INITIATOR_ENABLE
+    friend class ot::LinkMetrics::Initiator;
 #endif
 
 public:
@@ -610,7 +610,7 @@
      * @returns A reference to the MLE counters.
      *
      */
-    const otMleCounters &GetCounters(void)
+    const Counters &GetCounters(void)
     {
 #if OPENTHREAD_CONFIG_UPTIME_ENABLE
         UpdateRoleTimeCounters(mRole);
@@ -622,8 +622,9 @@
      * This method resets the MLE counters.
      *
      */
-    void ResetCounters(void) { memset(&mCounters, 0, sizeof(mCounters)); }
+    void ResetCounters(void);
 
+#if OPENTHREAD_CONFIG_MLE_PARENT_RESPONSE_CALLBACK_API_ENABLE
     /**
      * This function registers the client callback that is called when processing an MLE Parent Response message.
      *
@@ -635,6 +636,7 @@
     {
         mParentResponseCallback.Set(aCallback, aContext);
     }
+#endif
 
     /**
      * This method requests MLE layer to prepare and send a shorter version of Child ID Request message by only
@@ -1256,7 +1258,7 @@
          * @retval kErrorNoBufs   Insufficient buffers available to append the Connectivity TLV.
          *
          */
-        Error AppendAddresseRegisterationTlv(Child &aChild);
+        Error AppendAddressRegistrationTlv(Child &aChild);
 #endif // OPENTHREAD_FTD
 
         /**
@@ -1281,6 +1283,10 @@
          *
          */
         Error SendAfterDelay(const Ip6::Address &aDestination, uint16_t aDelay);
+
+    private:
+        Error AppendCompressedAddressEntry(uint8_t aContextId, const Ip6::Address &aAddress);
+        Error AppendAddressEntry(const Ip6::Address &aAddress);
     };
 
     /**
@@ -1519,59 +1525,40 @@
      */
     Mac::ShortAddress GetNextHop(uint16_t aDestination) const;
 
-#if OPENTHREAD_CONFIG_MLE_LINK_METRICS_INITIATOR_ENABLE || OPENTHREAD_CONFIG_MLE_LINK_METRICS_SUBJECT_ENABLE
     /**
-     * This method generates an MLE Data Request message which includes a Link Metrics Query TLV.
+     * This method generates an MLE Data Request message.
+     *
+     * @param[in]  aDestination      The IPv6 destination address.
+     *
+     * @retval kErrorNone     Successfully generated an MLE Data Request message.
+     * @retval kErrorNoBufs   Insufficient buffers to generate the MLE Data Request message.
+     *
+     */
+    Error SendDataRequest(const Ip6::Address &aDestination);
+
+#if OPENTHREAD_CONFIG_MLE_LINK_METRICS_INITIATOR_ENABLE
+    /**
+     * This method generates an MLE Data Request message which request Link Metrics Report TLV.
      *
      * @param[in]  aDestination      A reference to the IPv6 address of the destination.
-     * @param[in]  aTlvs             A pointer to requested TLV types.
-     * @param[in]  aTlvsLength       The number of TLV types in @p aTlvs.
-     * @param[in]  aDelay            Delay in milliseconds before the Data Request message is sent.
      * @param[in]  aQueryInfo        A Link Metrics query info.
      *
      * @retval kErrorNone     Successfully generated an MLE Data Request message.
      * @retval kErrorNoBufs   Insufficient buffers to generate the MLE Data Request message.
      *
      */
-    Error SendDataRequest(const Ip6::Address                        &aDestination,
-                          const uint8_t                             *aTlvs,
-                          uint8_t                                    aTlvsLength,
-                          uint16_t                                   aDelay,
-                          const LinkMetrics::LinkMetrics::QueryInfo &aQueryInfo)
-    {
-        return SendDataRequest(aDestination, aTlvs, aTlvsLength, aDelay, &aQueryInfo);
-    }
+    Error SendDataRequestForLinkMetricsReport(const Ip6::Address                      &aDestination,
+                                              const LinkMetrics::Initiator::QueryInfo &aQueryInfo);
 #endif
 
     /**
-     * This method generates an MLE Data Request message.
-     *
-     * @tparam kArrayLength          The TLV array length.
-     *
-     * @param[in]  aDestination      A reference to the IPv6 address of the destination.
-     * @param[in]  aTlvs             An array of requested TLVs.
-     * @param[in]  aDelay            Delay in milliseconds before the Data Request message is sent.
-     *
-     * @retval kErrorNone     Successfully generated an MLE Data Request message.
-     * @retval kErrorNoBufs   Insufficient buffers to generate the MLE Data Request message.
-     *
-     */
-    template <uint8_t kArrayLength>
-    Error SendDataRequest(const Ip6::Address &aDestination, const uint8_t (&aTlvs)[kArrayLength], uint16_t aDelay = 0)
-    {
-        return SendDataRequest(aDestination, aTlvs, kArrayLength, aDelay);
-    }
-
-    /**
      * This method generates an MLE Child Update Request message.
      *
-     * @param[in] aAppendChallenge   Indicates whether or not to include a Challenge TLV (even when already attached).
-     *
      * @retval kErrorNone    Successfully generated an MLE Child Update Request message.
      * @retval kErrorNoBufs  Insufficient buffers to generate the MLE Child Update Request message.
      *
      */
-    Error SendChildUpdateRequest(bool aAppendChallenge = false);
+    Error SendChildUpdateRequest(void);
 
     /**
      * This method generates an MLE Child Update Response message.
@@ -1765,20 +1752,21 @@
 
     Ip6::Netif::UnicastAddress mLeaderAloc; ///< Leader anycast locator
 
-    LeaderData    mLeaderData;               ///< Last received Leader Data TLV.
-    bool          mRetrieveNewNetworkData;   ///< Indicating new Network Data is needed if set.
-    DeviceRole    mRole;                     ///< Current Thread role.
-    Parent        mParent;                   ///< Parent information.
-    NeighborTable mNeighborTable;            ///< The neighbor table.
-    DeviceMode    mDeviceMode;               ///< Device mode setting.
-    AttachState   mAttachState;              ///< The attach state.
-    uint8_t       mParentRequestCounter;     ///< Number of parent requests while in `kAttachStateParentRequest`.
-    ReattachState mReattachState;            ///< Reattach state
-    uint16_t      mAttachCounter;            ///< Attach attempt counter.
-    uint16_t      mAnnounceDelay;            ///< Delay in between sending Announce messages during attach.
-    AttachTimer   mAttachTimer;              ///< The timer for driving the attach process.
-    DelayTimer    mDelayedResponseTimer;     ///< The timer to delay MLE responses.
-    MsgTxTimer    mMessageTransmissionTimer; ///< The timer for (re-)sending of MLE messages (e.g. Child Update).
+    LeaderData    mLeaderData;                 ///< Last received Leader Data TLV.
+    bool          mRetrieveNewNetworkData : 1; ///< Indicating new Network Data is needed if set.
+    bool          mRequestRouteTlv : 1;        ///< Request Route TLV when sending Data Request.
+    DeviceRole    mRole;                       ///< Current Thread role.
+    Parent        mParent;                     ///< Parent information.
+    NeighborTable mNeighborTable;              ///< The neighbor table.
+    DeviceMode    mDeviceMode;                 ///< Device mode setting.
+    AttachState   mAttachState;                ///< The attach state.
+    uint8_t       mParentRequestCounter;       ///< Number of parent requests while in `kAttachStateParentRequest`.
+    ReattachState mReattachState;              ///< Reattach state
+    uint16_t      mAttachCounter;              ///< Attach attempt counter.
+    uint16_t      mAnnounceDelay;              ///< Delay in between sending Announce messages during attach.
+    AttachTimer   mAttachTimer;                ///< The timer for driving the attach process.
+    DelayTimer    mDelayedResponseTimer;       ///< The timer to delay MLE responses.
+    MsgTxTimer    mMessageTransmissionTimer;   ///< The timer for (re-)sending of MLE messages (e.g. Child Update).
 #if OPENTHREAD_FTD
     uint8_t mLinkRequestAttempts; ///< Number of remaining link requests to send after reset.
     bool    mWasLeader;           ///< Indicating if device was leader before reset.
@@ -1846,6 +1834,13 @@
         kChildUpdateRequestActive,  // Child Update Request has been sent and Child Update Response is expected.
     };
 
+    enum ChildUpdateRequestMode : uint8_t // Used in `SendChildUpdateRequest()`
+    {
+        kNormalChildUpdateRequest, // Normal Child Update Request.
+        kAppendChallengeTlv,       // Append Challenge TLV to Child Update Request even if currently attached.
+        kAppendZeroTimeout,        // Use zero timeout when appending Timeout TLV (used for graceful detach).
+    };
+
     enum DataRequestState : uint8_t
     {
         kDataRequestNone,   // Not waiting for a Data Response.
@@ -1956,7 +1951,7 @@
         static constexpr uint32_t kCheckInterval   = (OPENTHREAD_CONFIG_PARENT_SEARCH_CHECK_INTERVAL * 1000u);
         static constexpr uint32_t kBackoffInterval = (OPENTHREAD_CONFIG_PARENT_SEARCH_BACKOFF_INTERVAL * 1000u);
         static constexpr uint32_t kJitterInterval  = (15 * 1000u);
-        static constexpr int8_t   kRssThreadhold   = OPENTHREAD_CONFIG_PARENT_SEARCH_RSS_THRESHOLD;
+        static constexpr int8_t   kRssThreshold    = OPENTHREAD_CONFIG_PARENT_SEARCH_RSS_THRESHOLD;
 
         using SearchTimer = TimerMilliIn<Mle, &Mle::HandleParentSearchTimer>;
 
@@ -1978,14 +1973,15 @@
     static void HandleDetachGracefullyTimer(Timer &aTimer);
     void        HandleDetachGracefullyTimer(void);
     bool        IsDetachingGracefully(void) { return mDetachGracefullyTimer.IsRunning(); }
-    Error       SendChildUpdateRequest(bool aAppendChallenge, uint32_t aTimeout);
+    Error       SendChildUpdateRequest(ChildUpdateRequestMode aMode);
+    Error       SendDataRequestAfterDelay(const Ip6::Address &aDestination, uint16_t aDelay);
 
-#if OPENTHREAD_CONFIG_MLE_LINK_METRICS_INITIATOR_ENABLE || OPENTHREAD_CONFIG_MLE_LINK_METRICS_SUBJECT_ENABLE
-    Error SendDataRequest(const Ip6::Address                        &aDestination,
-                          const uint8_t                             *aTlvs,
-                          uint8_t                                    aTlvsLength,
-                          uint16_t                                   aDelay,
-                          const LinkMetrics::LinkMetrics::QueryInfo *aQueryInfo = nullptr);
+#if OPENTHREAD_CONFIG_MLE_LINK_METRICS_INITIATOR_ENABLE
+    Error SendDataRequest(const Ip6::Address                      &aDestination,
+                          const uint8_t                           *aTlvs,
+                          uint8_t                                  aTlvsLength,
+                          uint16_t                                 aDelay,
+                          const LinkMetrics::Initiator::QueryInfo *aQueryInfo = nullptr);
 #else
     Error SendDataRequest(const Ip6::Address &aDestination, const uint8_t *aTlvs, uint8_t aTlvsLength, uint16_t aDelay);
 #endif
@@ -2007,13 +2003,11 @@
     void HandleAnnounce(RxInfo &aRxInfo);
 #if OPENTHREAD_CONFIG_MLE_LINK_METRICS_SUBJECT_ENABLE
     void HandleLinkMetricsManagementRequest(RxInfo &aRxInfo);
+    void HandleLinkProbe(RxInfo &aRxInfo);
 #endif
 #if OPENTHREAD_CONFIG_MLE_LINK_METRICS_INITIATOR_ENABLE
     void HandleLinkMetricsManagementResponse(RxInfo &aRxInfo);
 #endif
-#if OPENTHREAD_CONFIG_MLE_LINK_METRICS_SUBJECT_ENABLE
-    void HandleLinkProbe(RxInfo &aRxInfo);
-#endif
     Error HandleLeaderData(RxInfo &aRxInfo);
     void  ProcessAnnounce(void);
     bool  HasUnregisteredAddress(void);
@@ -2021,8 +2015,8 @@
     uint32_t GetAttachStartDelay(void) const;
     void     SendParentRequest(ParentRequestType aType);
     Error    SendChildIdRequest(void);
-    Error    GetNextAnnouceChannel(uint8_t &aChannel) const;
-    bool     HasMoreChannelsToAnnouce(void) const;
+    Error    GetNextAnnounceChannel(uint8_t &aChannel) const;
+    bool     HasMoreChannelsToAnnounce(void) const;
     bool     PrepareAnnounceState(void);
     void     SendAnnounce(uint8_t aChannel, AnnounceMode aMode);
     void     SendAnnounce(uint8_t aChannel, const Ip6::Address &aDestination, AnnounceMode aMode = kNormalAnnounce);
@@ -2111,10 +2105,11 @@
     ServiceAloc mServiceAlocs[kMaxServiceAlocs];
 #endif
 
-    otMleCounters mCounters;
+    Counters mCounters;
 #if OPENTHREAD_CONFIG_UPTIME_ENABLE
     uint64_t mLastUpdatedTimestamp;
 #endif
+
     static const otMeshLocalPrefix sMeshLocalPrefixInit;
 
     Ip6::Netif::UnicastAddress   mLinkLocal64;
@@ -2126,7 +2121,9 @@
     DetachGracefullyTimer                mDetachGracefullyTimer;
     Callback<otDetachGracefullyCallback> mDetachGracefullyCallback;
 
+#if OPENTHREAD_CONFIG_MLE_PARENT_RESPONSE_CALLBACK_API_ENABLE
     Callback<otThreadParentResponseCallback> mParentResponseCallback;
+#endif
 };
 
 } // namespace Mle
diff --git a/src/core/thread/mle_router.cpp b/src/core/thread/mle_router.cpp
index 935451d..417c1c5 100644
--- a/src/core/thread/mle_router.cpp
+++ b/src/core/thread/mle_router.cpp
@@ -808,8 +808,6 @@
 
 Error MleRouter::HandleLinkAccept(RxInfo &aRxInfo, bool aRequest)
 {
-    static const uint8_t kDataRequestTlvs[] = {Tlv::kNetworkData};
-
     Error           error = kErrorNone;
     Router         *router;
     Neighbor::State neighborState;
@@ -916,7 +914,7 @@
 
         mLinkRequestAttempts    = 0; // completed router sync after reset, no more link request to retransmit
         mRetrieveNewNetworkData = true;
-        IgnoreError(SendDataRequest(aRxInfo.mMessageInfo.GetPeerAddr(), kDataRequestTlvs));
+        IgnoreError(SendDataRequest(aRxInfo.mMessageInfo.GetPeerAddr()));
 
 #if OPENTHREAD_CONFIG_TIME_SYNC_ENABLE
         Get<TimeSync>().HandleTimeSyncMessage(aRxInfo.mMessage);
@@ -939,7 +937,7 @@
             SerialNumber::IsGreater(leaderData.GetDataVersion(NetworkData::kFullSet),
                                     Get<NetworkData::Leader>().GetVersion(NetworkData::kFullSet)))
         {
-            IgnoreError(SendDataRequest(aRxInfo.mMessageInfo.GetPeerAddr(), kDataRequestTlvs));
+            IgnoreError(SendDataRequest(aRxInfo.mMessageInfo.GetPeerAddr()));
         }
 
         // Route (optional)
@@ -1085,6 +1083,7 @@
     case kErrorNone:
         SuccessOrExit(error = ProcessRouteTlv(routeTlv, aRxInfo));
         mRouterTable.UpdateRoutesOnFed(routeTlv, aParentId);
+        mRequestRouteTlv = false;
         break;
     case kErrorNotFound:
         break;
@@ -1134,7 +1133,7 @@
     // only when device is attached (in child, router, or leader roles)
     // and `IsFullThreadDevice()`.
     //
-    // - `aSourceAdress` is the read value from `SourceAddressTlv`.
+    // - `aSourceAddress` is the read value from `SourceAddressTlv`.
     // - `aLeaderData` is the read value from `LeaderDataTlv`.
 
     Error    error      = kErrorNone;
@@ -1316,6 +1315,10 @@
                                          DeviceMode::kModeFullNetworkData));
 
         mNeighborTable.Signal(NeighborTable::kRouterAdded, *router);
+
+        // Change the cache entries associated with the former child
+        // from using the old RLOC16 to its new RLOC16.
+        Get<AddressResolver>().ReplaceEntriesForRloc16(aRxInfo.mNeighbor->GetRloc16(), router->GetRloc16());
     }
 
     // Send unicast link request if no link to router and no unicast/multicast link request in progress
@@ -1785,26 +1788,28 @@
 }
 #endif
 
-Error MleRouter::UpdateChildAddresses(const Message &aMessage, uint16_t aOffset, uint16_t aLength, Child &aChild)
+Error MleRouter::ProcessAddressRegistrationTlv(RxInfo &aRxInfo, Child &aChild)
 {
-    Error                    error = kErrorNone;
-    AddressRegistrationEntry entry;
-    Ip6::Address             address;
-    Lowpan::Context          context;
-    uint8_t                  registeredCount = 0;
-    uint8_t                  storedCount     = 0;
-    uint16_t                 end             = aOffset + aLength;
+    Error    error;
+    uint16_t offset;
+    uint16_t length;
+    uint16_t endOffset;
+    uint8_t  count       = 0;
+    uint8_t  storedCount = 0;
 #if OPENTHREAD_CONFIG_TMF_PROXY_DUA_ENABLE
     Ip6::Address        oldDua;
     const Ip6::Address *oldDuaPtr = nullptr;
     bool                hasDua    = false;
 #endif
-
 #if OPENTHREAD_CONFIG_TMF_PROXY_MLR_ENABLE
     Ip6::Address oldMlrRegisteredAddresses[kMaxChildIpAddresses - 1];
     uint16_t     oldMlrRegisteredAddressNum = 0;
 #endif
 
+    SuccessOrExit(error = Tlv::FindTlvValueOffset(aRxInfo.mMessage, Tlv::kAddressRegistration, offset, length));
+
+    endOffset = offset + length;
+
 #if OPENTHREAD_CONFIG_TMF_PROXY_DUA_ENABLE
     if ((oldDuaPtr = aChild.GetDomainUnicastAddress()) != nullptr)
     {
@@ -1831,36 +1836,47 @@
 
     aChild.ClearIp6Addresses();
 
-    while (aOffset < end)
+    while (offset < endOffset)
     {
-        uint8_t len;
+        uint8_t      controlByte;
+        Ip6::Address address;
 
-        // read out the control field
-        SuccessOrExit(error = aMessage.Read(aOffset, &entry, sizeof(uint8_t)));
+        // Read out the control byte (first byte in entry)
+        SuccessOrExit(error = aRxInfo.mMessage.Read(offset, controlByte));
+        offset++;
+        count++;
 
-        len = entry.GetLength();
+        address.Clear();
 
-        SuccessOrExit(error = aMessage.Read(aOffset, &entry, len));
-
-        aOffset += len;
-        registeredCount++;
-
-        if (entry.IsCompressed())
+        if (AddressRegistrationTlv::IsEntryCompressed(controlByte))
         {
-            if (Get<NetworkData::Leader>().GetContext(entry.GetContextId(), context) != kErrorNone)
+            // Compressed entry contains IID with the 64-bit prefix
+            // determined from 6LoWPAN context identifier (from
+            // the control byte).
+
+            uint8_t         contextId = AddressRegistrationTlv::GetContextId(controlByte);
+            Lowpan::Context context;
+
+            VerifyOrExit(offset + sizeof(Ip6::InterfaceIdentifier) <= endOffset, error = kErrorParse);
+            IgnoreError(aRxInfo.mMessage.Read(offset, address.GetIid()));
+            offset += sizeof(Ip6::InterfaceIdentifier);
+
+            if (Get<NetworkData::Leader>().GetContext(contextId, context) != kErrorNone)
             {
-                LogWarn("Failed to get context %u for compressed address from child 0x%04x", entry.GetContextId(),
+                LogWarn("Failed to get context %u for compressed address from child 0x%04x", contextId,
                         aChild.GetRloc16());
                 continue;
             }
 
-            address.Clear();
             address.SetPrefix(context.mPrefix);
-            address.SetIid(entry.GetIid());
         }
         else
         {
-            address = entry.GetIp6Address();
+            // Uncompressed entry contains the full IPv6 address.
+
+            VerifyOrExit(offset + sizeof(Ip6::Address) <= endOffset, error = kErrorParse);
+            IgnoreError(aRxInfo.mMessage.Read(offset, address));
+            offset += sizeof(Ip6::Address);
         }
 
 #if OPENTHREAD_CONFIG_REFERENCE_DEVICE_ENABLE
@@ -1942,7 +1958,7 @@
         }
 
         // Clear EID-to-RLOC cache for the unicast address registered by the child.
-        Get<AddressResolver>().Remove(address);
+        Get<AddressResolver>().RemoveEntryForAddress(address);
     }
 #if OPENTHREAD_CONFIG_TMF_PROXY_DUA_ENABLE
     // Dua is removed
@@ -1956,14 +1972,14 @@
     Get<MlrManager>().UpdateProxiedSubscriptions(aChild, oldMlrRegisteredAddresses, oldMlrRegisteredAddressNum);
 #endif
 
-    if (registeredCount == 0)
+    if (count == 0)
     {
         LogInfo("Child 0x%04x has no registered IPv6 address", aChild.GetRloc16());
     }
     else
     {
-        LogInfo("Child 0x%04x has %u registered IPv6 address%s, %u address%s stored", aChild.GetRloc16(),
-                registeredCount, (registeredCount == 1) ? "" : "es", storedCount, (storedCount == 1) ? "" : "es");
+        LogInfo("Child 0x%04x has %u registered IPv6 address%s, %u address%s stored", aChild.GetRloc16(), count,
+                (count == 1) ? "" : "es", storedCount, (storedCount == 1) ? "" : "es");
     }
 
     error = kErrorNone;
@@ -1987,6 +2003,7 @@
     MeshCoP::Timestamp timestamp;
     bool               needsActiveDatasetTlv;
     bool               needsPendingDatasetTlv;
+    bool               needsSupervisionTlv;
     Child             *child;
     Router            *router;
     uint8_t            numTlvs;
@@ -2030,9 +2047,11 @@
     SuccessOrExit(error = Tlv::Find<TimeoutTlv>(aRxInfo.mMessage, timeout));
 
     // Supervision interval
+    needsSupervisionTlv = false;
     switch (Tlv::Find<SupervisionIntervalTlv>(aRxInfo.mMessage, supervisionInterval))
     {
     case kErrorNone:
+        needsSupervisionTlv = true;
         break;
     case kErrorNotFound:
         supervisionInterval = (version <= kThreadVersion1p3) ? kChildSupervisionDefaultIntervalForOlderVersion : 0;
@@ -2084,15 +2103,16 @@
         numTlvs++;
     }
 
+    if (needsSupervisionTlv)
+    {
+        numTlvs++;
+    }
+
     VerifyOrExit(numTlvs <= Child::kMaxRequestTlvs, error = kErrorParse);
 
     if (!mode.IsFullThreadDevice())
     {
-        uint16_t offset;
-        uint16_t length;
-
-        SuccessOrExit(error = Tlv::FindTlvValueOffset(aRxInfo.mMessage, Tlv::kAddressRegistration, offset, length));
-        SuccessOrExit(error = UpdateChildAddresses(aRxInfo.mMessage, offset, length, *child));
+        SuccessOrExit(error = ProcessAddressRegistrationTlv(aRxInfo, *child));
     }
 
     // Remove from router table
@@ -2145,6 +2165,11 @@
         child->SetRequestTlv(numTlvs++, Tlv::kPendingDataset);
     }
 
+    if (needsSupervisionTlv)
+    {
+        child->SetRequestTlv(numTlvs++, Tlv::kSupervisionInterval);
+    }
+
     aRxInfo.mClass = RxInfo::kAuthoritativeMessage;
 
     switch (mRole)
@@ -2182,8 +2207,6 @@
     DeviceMode      oldMode;
     TlvList         requestedTlvList;
     TlvList         tlvList;
-    uint16_t        addrOffset;
-    uint16_t        addrLength;
     bool            childDidChange = false;
 
     Log(kMessageReceive, kTypeChildUpdateRequestOfChild, aRxInfo.mMessageInfo.GetPeerAddr());
@@ -2246,10 +2269,15 @@
     }
 
     // IPv6 Address TLV
-    if (Tlv::FindTlvValueOffset(aRxInfo.mMessage, Tlv::kAddressRegistration, addrOffset, addrLength) == kErrorNone)
+    switch (ProcessAddressRegistrationTlv(aRxInfo, *child))
     {
-        SuccessOrExit(error = UpdateChildAddresses(aRxInfo.mMessage, addrOffset, addrLength, *child));
+    case kErrorNone:
         tlvList.Add(Tlv::kAddressRegistration);
+        break;
+    case kErrorNotFound:
+        break;
+    default:
+        ExitNow(error = kErrorParse);
     }
 
     // Leader Data
@@ -2404,8 +2432,6 @@
     uint32_t   mleFrameCounter;
     LeaderData leaderData;
     Child     *child;
-    uint16_t   addrOffset;
-    uint16_t   addrLength;
 
     if ((aRxInfo.mNeighbor == nullptr) || IsActiveRouter(aRxInfo.mNeighbor->GetRloc16()) ||
         !Get<ChildTable>().Contains(*aRxInfo.mNeighbor))
@@ -2517,9 +2543,13 @@
     }
 
     // IPv6 Address
-    if (Tlv::FindTlvValueOffset(aRxInfo.mMessage, Tlv::kAddressRegistration, addrOffset, addrLength) == kErrorNone)
+    switch (ProcessAddressRegistrationTlv(aRxInfo, *child))
     {
-        SuccessOrExit(error = UpdateChildAddresses(aRxInfo.mMessage, addrOffset, addrLength, *child));
+    case kErrorNone:
+    case kErrorNotFound:
+        break;
+    default:
+        ExitNow(error = kErrorParse);
     }
 
     // Leader Data
@@ -2907,6 +2937,10 @@
             SuccessOrExit(error = message->AppendPendingDatasetTlv());
             break;
 
+        case Tlv::kSupervisionInterval:
+            SuccessOrExit(error = message->AppendSupervisionIntervalTlv(aChild.GetSupervisionInterval()));
+            break;
+
         default:
             break;
         }
@@ -2914,7 +2948,7 @@
 
     if (!aChild.IsFullThreadDevice())
     {
-        SuccessOrExit(error = message->AppendAddresseRegisterationTlv(aChild));
+        SuccessOrExit(error = message->AppendAddressRegistrationTlv(aChild));
     }
 
     SetChildStateToValid(aChild);
@@ -3053,7 +3087,7 @@
         switch (tlvType)
         {
         case Tlv::kAddressRegistration:
-            SuccessOrExit(error = message->AppendAddresseRegisterationTlv(*aChild));
+            SuccessOrExit(error = message->AppendAddressRegistrationTlv(*aChild));
             break;
 
         case Tlv::kMode:
@@ -3141,12 +3175,16 @@
             SuccessOrExit(error = message->AppendPendingDatasetTlv());
             break;
 
+        case Tlv::kRoute:
+            SuccessOrExit(error = message->AppendRouteTlv());
+            break;
+
 #if OPENTHREAD_CONFIG_MLE_LINK_METRICS_SUBJECT_ENABLE
         case Tlv::kLinkMetricsReport:
             OT_ASSERT(aRequestMessage != nullptr);
             neighbor = mNeighborTable.FindNeighbor(aDestination);
             VerifyOrExit(neighbor != nullptr, error = kErrorInvalidState);
-            SuccessOrExit(error = Get<LinkMetrics::LinkMetrics>().AppendReport(*message, *aRequestMessage, *neighbor));
+            SuccessOrExit(error = Get<LinkMetrics::Subject>().AppendReport(*message, *aRequestMessage, *neighbor));
             break;
 #endif
         }
@@ -3240,7 +3278,7 @@
         if (aNeighbor.IsFullThreadDevice())
         {
             // Clear all EID-to-RLOC entries associated with the child.
-            Get<AddressResolver>().Remove(aNeighbor.GetRloc16());
+            Get<AddressResolver>().RemoveEntriesForRloc16(aNeighbor.GetRloc16());
         }
 
         mChildTable.RemoveStoredChild(static_cast<Child &>(aNeighbor));
@@ -3661,6 +3699,21 @@
 
     Log(kMessageSend, kTypeAddressReply, aMessageInfo.GetPeerAddr());
 
+    // If assigning a new RLOC16 (e.g., on promotion of a child to
+    // router role) we clear any address cache entries associated
+    // with the old RLOC16.
+
+    if ((aResponseStatus == ThreadStatusTlv::kSuccess) && (aRouter != nullptr))
+    {
+        uint16_t oldRloc16;
+
+        VerifyOrExit(IsRoutingLocator(aMessageInfo.GetPeerAddr()));
+        oldRloc16 = aMessageInfo.GetPeerAddr().GetIid().GetLocator();
+
+        VerifyOrExit(oldRloc16 != aRouter->GetRloc16());
+        Get<AddressResolver>().RemoveEntriesForRloc16(oldRloc16);
+    }
+
 exit:
     FreeMessage(message);
 }
diff --git a/src/core/thread/mle_router.hpp b/src/core/thread/mle_router.hpp
index 423550b..ac84f75 100644
--- a/src/core/thread/mle_router.hpp
+++ b/src/core/thread/mle_router.hpp
@@ -630,6 +630,7 @@
     void  SetStateRouterOrLeader(DeviceRole aRole, uint16_t aRloc16, LeaderStartMode aStartMode);
     void  StopLeader(void);
     void  SynchronizeChildNetworkData(void);
+    Error ProcessAddressRegistrationTlv(RxInfo &aRxInfo, Child &aChild);
     Error UpdateChildAddresses(const Message &aMessage, uint16_t aOffset, uint16_t aLength, Child &aChild);
     bool  HasNeighborWithGoodLinkQuality(void) const;
 
diff --git a/src/core/thread/mle_tlvs.hpp b/src/core/thread/mle_tlvs.hpp
index 98956a6..7449965 100644
--- a/src/core/thread/mle_tlvs.hpp
+++ b/src/core/thread/mle_tlvs.hpp
@@ -975,96 +975,57 @@
 };
 
 /**
- * This class implements Source Address TLV generation and parsing.
+ * This class provides constants and methods for generation and parsing of Address Registration TLV.
  *
  */
-OT_TOOL_PACKED_BEGIN
-class AddressRegistrationEntry
+class AddressRegistrationTlv : public TlvInfo<Tlv::kAddressRegistration>
 {
 public:
     /**
-     * This method returns the IPv6 address or IID length.
-     *
-     * @returns The IPv6 address length if the Compressed bit is clear, or the IID length if the Compressed bit is
-     *          set.
+     * This constant defines the control byte to use in an uncompressed entry where the full IPv6 address is included in
+     * the TLV.
      *
      */
-    uint8_t GetLength(void) const { return sizeof(mControl) + (IsCompressed() ? sizeof(mIid) : sizeof(mIp6Address)); }
+    static constexpr uint8_t kControlByteUncompressed = 0;
 
     /**
-     * This method indicates whether or not the Compressed flag is set.
+     * This static method returns the control byte to use in a compressed entry where the 64-prefix is replaced with a
+     * 6LoWPAN context identifier.
      *
-     * @retval TRUE   If the Compressed flag is set.
-     * @retval FALSE  If the Compressed flag is not set.
+     * @param[in] aContextId   The 6LoWPAN context ID.
+     *
+     * @returns The control byte associated with compressed entry with @p aContextId.
      *
      */
-    bool IsCompressed(void) const { return (mControl & kCompressed) != 0; }
+    static uint8_t ControlByteFor(uint8_t aContextId) { return kCompressed | (aContextId & kContextIdMask); }
 
     /**
-     * This method sets the Uncompressed flag.
+     * This static method indicates whether or not an address entry is using compressed format.
+     *
+     * @param[in] aControlByte  The control byte (the first byte in the entry).
+     *
+     * @retval TRUE   If the entry uses compressed format.
+     * @retval FALSE  If the entry uses uncompressed format.
      *
      */
-    void SetUncompressed(void) { mControl = 0; }
+    static bool IsEntryCompressed(uint8_t aControlByte) { return (aControlByte & kCompressed); }
 
     /**
-     * This method returns the Context ID for the compressed form.
+     * This static method gets the context ID in a compressed entry.
      *
-     * @returns The Context ID value.
+     * @param[in] aControlByte  The control byte (the first byte in the entry).
+     *
+     * @returns The 6LoWPAN context ID.
      *
      */
-    uint8_t GetContextId(void) const { return mControl & kCidMask; }
+    static uint8_t GetContextId(uint8_t aControlByte) { return (aControlByte & kContextIdMask); }
 
-    /**
-     * This method sets the Context ID value.
-     *
-     * @param[in]  aContextId  The Context ID value.
-     *
-     */
-    void SetContextId(uint8_t aContextId) { mControl = kCompressed | aContextId; }
-
-    /**
-     * This method returns the IID value.
-     *
-     * @returns The IID value.
-     *
-     */
-    const Ip6::InterfaceIdentifier &GetIid(void) const { return mIid; }
-
-    /**
-     * This method sets the IID value.
-     *
-     * @param[in]  aIid  The IID value.
-     *
-     */
-    void SetIid(const Ip6::InterfaceIdentifier &aIid) { mIid = aIid; }
-
-    /**
-     * This method returns the IPv6 Address value.
-     *
-     * @returns The IPv6 Address value.
-     *
-     */
-    const Ip6::Address &GetIp6Address(void) const { return mIp6Address; }
-
-    /**
-     * This method sets the IPv6 Address value.
-     *
-     * @param[in]  aAddress  A reference to the IPv6 Address value.
-     *
-     */
-    void SetIp6Address(const Ip6::Address &aAddress) { mIp6Address = aAddress; }
+    AddressRegistrationTlv(void) = delete;
 
 private:
-    static constexpr uint8_t kCompressed = 1 << 7;
-    static constexpr uint8_t kCidMask    = 0xf;
-
-    uint8_t mControl;
-    union
-    {
-        Ip6::InterfaceIdentifier mIid;
-        Ip6::Address             mIp6Address;
-    } OT_TOOL_PACKED_FIELD;
-} OT_TOOL_PACKED_END;
+    static constexpr uint8_t kCompressed    = 1 << 7;
+    static constexpr uint8_t kContextIdMask = 0xf;
+};
 
 /**
  * This class implements Channel TLV generation and parsing.
diff --git a/src/core/thread/mle_types.hpp b/src/core/thread/mle_types.hpp
index b454397..286e63a 100644
--- a/src/core/thread/mle_types.hpp
+++ b/src/core/thread/mle_types.hpp
@@ -647,6 +647,12 @@
 typedef Mac::Key Key;
 
 /**
+ * This structure represents the Thread MLE counters.
+ *
+ */
+typedef otMleCounters Counters;
+
+/**
  * This function derives the Child ID from a given RLOC16.
  *
  * @param[in]  aRloc16  The RLOC16 value.
diff --git a/src/core/thread/mlr_manager.cpp b/src/core/thread/mlr_manager.cpp
index befb601..d8e8a03 100644
--- a/src/core/thread/mlr_manager.cpp
+++ b/src/core/thread/mlr_manager.cpp
@@ -289,7 +289,7 @@
 
     mMlrPending = true;
 
-    // Generally Thread 1.2 Router would send MLR.req on bebelf for MA (scope >=4) subscribed by its MTD child.
+    // Generally Thread 1.2 Router would send MLR.req on behalf for MA (scope >=4) subscribed by its MTD child.
     // When Thread 1.2 MTD attaches to Thread 1.1 parent, 1.2 MTD should send MLR.req to PBBR itself.
     // In this case, Thread 1.2 sleepy end device relies on fast data poll to fetch the response timely.
     if (!Get<Mle::Mle>().IsRxOnWhenIdle())
diff --git a/src/core/thread/network_data.cpp b/src/core/thread/network_data.cpp
index 2ef2c33..009e675 100644
--- a/src/core/thread/network_data.cpp
+++ b/src/core/thread/network_data.cpp
@@ -190,7 +190,7 @@
 
         for (const NetworkDataTlv *subCur; subCur = iterator.GetSubTlv(subTlvs),
                                            (subCur + 1 <= cur->GetNext()) && (subCur->GetNext() <= cur->GetNext());
-             iterator.AdvaceSubTlv(subTlvs))
+             iterator.AdvanceSubTlv(subTlvs))
         {
             if (cur->GetType() == NetworkDataTlv::kTypePrefix)
             {
diff --git a/src/core/thread/network_data.hpp b/src/core/thread/network_data.hpp
index 72e44a5..3c1db30 100644
--- a/src/core/thread/network_data.hpp
+++ b/src/core/thread/network_data.hpp
@@ -527,7 +527,7 @@
                                                             GetSubTlvOffset());
         }
 
-        void AdvaceSubTlv(const NetworkDataTlv *aSubTlvs)
+        void AdvanceSubTlv(const NetworkDataTlv *aSubTlvs)
         {
             SaveSubTlvOffset(GetSubTlv(aSubTlvs)->GetNext(), aSubTlvs);
             SetEntryIndex(0);
diff --git a/src/core/thread/network_data_leader.cpp b/src/core/thread/network_data_leader.cpp
index 95d20ba..351ea69 100644
--- a/src/core/thread/network_data_leader.cpp
+++ b/src/core/thread/network_data_leader.cpp
@@ -296,6 +296,11 @@
     // that the first entry is preferred over the second one.
 
     result = ThreeWayCompare(Get<RouterTable>().GetPathCost(aSecondRloc), Get<RouterTable>().GetPathCost(aFirstRloc));
+    VerifyOrExit(result == 0);
+
+    // If all the same, prefer the BR acting as a router over an
+    // end device.
+    result = ThreeWayCompare(Mle::IsActiveRouter(aFirstRloc), Mle::IsActiveRouter(aSecondRloc));
 #endif
 
 exit:
diff --git a/src/core/thread/network_data_leader_ftd.cpp b/src/core/thread/network_data_leader_ftd.cpp
index 6a55343..39a033d 100644
--- a/src/core/thread/network_data_leader_ftd.cpp
+++ b/src/core/thread/network_data_leader_ftd.cpp
@@ -134,7 +134,7 @@
 
     VerifyOrExit(Get<Mle::Mle>().IsLeader() && !mWaitingForNetDataSync);
 
-    LogInfo("Received network data registration");
+    LogInfo("Received %s", UriToString<kUriServerData>());
 
     VerifyOrExit(aMessageInfo.GetPeerAddr().GetIid().IsRoutingLocator());
 
@@ -162,7 +162,7 @@
 
     SuccessOrExit(Get<Tmf::Agent>().SendEmptyAck(aMessage, aMessageInfo));
 
-    LogInfo("Sent network data registration acknowledgment");
+    LogInfo("Sent %s ack", UriToString<kUriServerData>());
 
 exit:
     return;
@@ -332,7 +332,7 @@
 
     SuccessOrExit(error = Get<Tmf::Agent>().SendMessage(*message, aMessageInfo));
 
-    LogInfo("sent commissioning dataset get response");
+    LogInfo("Sent %s response", UriToString<kUriCommissionerGet>());
 
 exit:
     FreeMessageOnError(message, error);
@@ -352,7 +352,7 @@
 
     SuccessOrExit(error = Get<Tmf::Agent>().SendMessage(*message, aMessageInfo));
 
-    LogInfo("sent commissioning dataset set response");
+    LogInfo("sent %s response", UriToString<kUriCommissionerSet>());
 
 exit:
     FreeMessageOnError(message, error);
diff --git a/src/core/thread/network_data_notifier.cpp b/src/core/thread/network_data_notifier.cpp
index e9befb4..65b76e0 100644
--- a/src/core/thread/network_data_notifier.cpp
+++ b/src/core/thread/network_data_notifier.cpp
@@ -211,7 +211,7 @@
     IgnoreError(messageInfo.SetSockAddrToRlocPeerAddrToLeaderAloc());
     SuccessOrExit(error = Get<Tmf::Agent>().SendMessage(*message, messageInfo, HandleCoapResponse, this));
 
-    LogInfo("Sent server data notification");
+    LogInfo("Sent %s", UriToString<kUriServerData>());
 
 exit:
     FreeMessageOnError(message, error);
diff --git a/src/core/thread/network_data_publisher.cpp b/src/core/thread/network_data_publisher.cpp
index 566bce7..056765c 100644
--- a/src/core/thread/network_data_publisher.cpp
+++ b/src/core/thread/network_data_publisher.cpp
@@ -64,8 +64,8 @@
 #if OPENTHREAD_CONFIG_BORDER_ROUTER_ENABLE
     // Since the `PrefixEntry` type is used in an array,
     // we cannot use a constructor with an argument (e.g.,
-    // we cannot use `InstacneLocator`) so we use
-    // `IntanceLocatorInit`  and `Init()` the entries one
+    // we cannot use `InstanceLocator`) so we use
+    // `InstanceLocatorInit`  and `Init()` the entries one
     // by one.
 
     for (PrefixEntry &entry : mPrefixEntries)
@@ -96,13 +96,20 @@
 
 Error Publisher::PublishExternalRoute(const ExternalRouteConfig &aConfig, Requester aRequester)
 {
+    return ReplacePublishedExternalRoute(aConfig.GetPrefix(), aConfig, aRequester);
+}
+
+Error Publisher::ReplacePublishedExternalRoute(const Ip6::Prefix         &aPrefix,
+                                               const ExternalRouteConfig &aConfig,
+                                               Requester                  aRequester)
+{
     Error        error = kErrorNone;
     PrefixEntry *entry;
 
     VerifyOrExit(aConfig.IsValid(GetInstance()), error = kErrorInvalidArgs);
     VerifyOrExit(aConfig.mStable, error = kErrorInvalidArgs);
 
-    entry = FindOrAllocatePrefixEntry(aConfig.GetPrefix(), aRequester);
+    entry = FindOrAllocatePrefixEntry(aPrefix, aRequester);
     VerifyOrExit(entry != nullptr, error = kErrorNoBufs);
 
     entry->Publish(aConfig, aRequester);
@@ -330,7 +337,7 @@
 
             if (aNumPreferredEntries < aDesiredNumEntries)
             {
-                mUpdateTime += kExtraDelayToRemovePeferred;
+                mUpdateTime += kExtraDelayToRemovePreferred;
             }
 
             SetState(kRemoving);
@@ -806,23 +813,24 @@
 
     if (GetState() != kNoEntry)
     {
-        // If this is an existing entry, first we check that there is
-        // a change in either type or flags. We remove the old entry
-        // from Network Data if it was added. If the only change is
-        // to flags (e.g., change to the preference level) and the
-        // entry was previously added in Network Data, we re-add it
-        // with the new flags. This ensures that changes to flags are
-        // immediately reflected in the Network Data.
+        // If this is an existing entry, check if there is a change in
+        // type, flags, or the prefix itself. If not, everything is
+        // as before. If something is different, first, remove the
+        // old entry from Network Data if it was added. Then, re-add
+        // the new prefix/flags (replacing the old entry). This
+        // ensures the changes are immediately reflected in the
+        // Network Data.
 
         State oldState = GetState();
 
-        VerifyOrExit((mType != aNewType) || (mFlags != aNewFlags));
+        VerifyOrExit((mType != aNewType) || (mFlags != aNewFlags) || (mPrefix != aPrefix));
 
         Remove(/* aNextState */ kNoEntry);
 
         if ((mType == aNewType) && ((oldState == kAdded) || (oldState == kRemoving)))
         {
-            mFlags = aNewFlags;
+            mPrefix = aPrefix;
+            mFlags  = aNewFlags;
             Add();
         }
     }
diff --git a/src/core/thread/network_data_publisher.hpp b/src/core/thread/network_data_publisher.hpp
index bd1dfbe..3d3a37f 100644
--- a/src/core/thread/network_data_publisher.hpp
+++ b/src/core/thread/network_data_publisher.hpp
@@ -267,6 +267,43 @@
     Error PublishExternalRoute(const ExternalRouteConfig &aConfig, Requester aRequester);
 
     /**
+     * This method replaces a previously published external route.
+     *
+     * Only stable entries can be published (i.e.,`aConfig.mStable` MUST be `true`).
+     *
+     * If there is no previously published external route matching @p aPrefix, this method behaves similarly to
+     * `PublishExternalRoute()`, i.e., it will start the process of publishing @a aConfig as an external route in the
+     * Thread Network Data.
+     *
+     * If there is a previously published route entry matching @p aPrefix, it will be replaced with the new prefix from
+     * @p aConfig.
+     *
+     * - If the @p aPrefix was already added in the Network Data, the change to the new prefix in @p aConfig is
+     *   immediately reflected in the Network Data. This ensures that route entries in the Network Data are not
+     *   abruptly removed and the transition from aPrefix to the new prefix is smooth.
+     *
+     * - If the old published @p aPrefix was not added in the Network Data, it will be replaced with the new @p aConfig
+     *   prefix but it will not be immediately added. Instead, it will start the process of publishing it in the
+     *   Network Data (monitoring the Network Data to determine when/if to add the prefix, depending on the number of
+     *   similar prefixes present in the Network Data).
+     *
+     * @param[in] aPrefix         The previously published external route prefix to replace.
+     * @param[in] aConfig         The external route config to publish.
+     * @param[in] aRequester      The requester (`kFromUser` or `kFromRoutingManager` module).
+     *
+     * @retval kErrorNone         The external route is published successfully.
+     * @retval kErrorInvalidArgs  The @p aConfig is not valid (bad prefix, invalid flag combinations, or not stable).
+     * @retval kErrorNoBufs       Could not allocate an entry for the new request. Publisher supports a limited number
+     *                            of entries (shared between on-mesh prefix and external route) determined by config
+     *                            `OPENTHREAD_CONFIG_NETDATA_PUBLISHER_MAX_PREFIX_ENTRIES`.
+     *
+     *
+     */
+    Error ReplacePublishedExternalRoute(const Ip6::Prefix         &aPrefix,
+                                        const ExternalRouteConfig &aConfig,
+                                        Requester                  aRequester);
+
+    /**
      * This method indicates whether or not currently a published prefix entry (on-mesh or external route) is added to
      * the Thread Network Data.
      *
@@ -306,7 +343,7 @@
         // All intervals are in milliseconds.
         static constexpr uint32_t kMaxDelayToAdd    = OPENTHREAD_CONFIG_NETDATA_PUBLISHER_MAX_DELAY_TO_ADD;
         static constexpr uint32_t kMaxDelayToRemove = OPENTHREAD_CONFIG_NETDATA_PUBLISHER_MAX_DELAY_TO_REMOVE;
-        static constexpr uint32_t kExtraDelayToRemovePeferred =
+        static constexpr uint32_t kExtraDelayToRemovePreferred =
             OPENTHREAD_CONFIG_NETDATA_PUBLISHER_EXTRA_DELAY_TIME_TO_REMOVE_PREFERRED;
 
         static constexpr uint16_t kInfoStringSize = 60;
diff --git a/src/core/thread/network_data_service.hpp b/src/core/thread/network_data_service.hpp
index 6695e9f..5dec83a 100644
--- a/src/core/thread/network_data_service.hpp
+++ b/src/core/thread/network_data_service.hpp
@@ -423,7 +423,7 @@
     /**
      * This method adds a Thread Service entry to the local Thread Network Data.
      *
-     * This version of `Add<SeviceType>()` is intended for use with a `ServiceType` that has a constant service data
+     * This version of `Add<ServiceType>()` is intended for use with a `ServiceType` that has a constant service data
      * format with a non-empty and potentially non-const server data format (provided as input parameter).
      *
      * The template type `ServiceType` has the following requirements:
@@ -450,7 +450,7 @@
     /**
      * This method adds a Thread Service entry to the local Thread Network Data.
      *
-     * This version of `Add<SeviceType>()` is intended for use with a `ServiceType` that has a non-const service data
+     * This version of `Add<ServiceType>()` is intended for use with a `ServiceType` that has a non-const service data
      * format (provided as input parameter) with an empty server data.
      *
      * The template type `ServiceType` has the following requirements:
@@ -495,8 +495,8 @@
     /**
      * This method removes a Thread Service entry from the local Thread Network Data.
      *
-     * This version of `Remove<SeviceType>()` is intended for use with a `ServiceType` that has a non-const service data
-     * format (provided as input parameter).
+     * This version of `Remove<ServiceType>()` is intended for use with a `ServiceType` that has a non-const service
+     * data format (provided as input parameter).
      *
      * The template type `ServiceType` has the following requirements:
      *   - It MUST define nested type `ServiceType::ServiceData` representing the service data (and its format).
diff --git a/src/core/thread/network_diagnostic.cpp b/src/core/thread/network_diagnostic.cpp
index c40c048..dfa8570 100644
--- a/src/core/thread/network_diagnostic.cpp
+++ b/src/core/thread/network_diagnostic.cpp
@@ -33,8 +33,6 @@
 
 #include "network_diagnostic.hpp"
 
-#if OPENTHREAD_FTD || OPENTHREAD_CONFIG_TMF_NETWORK_DIAG_MTD_ENABLE
-
 #include "coap/coap_message.hpp"
 #include "common/array.hpp"
 #include "common/as_core_type.hpp"
@@ -58,82 +56,66 @@
 
 namespace NetworkDiagnostic {
 
-NetworkDiagnostic::NetworkDiagnostic(Instance &aInstance)
+const char Server::kVendorName[]      = OPENTHREAD_CONFIG_NET_DIAG_VENDOR_NAME;
+const char Server::kVendorModel[]     = OPENTHREAD_CONFIG_NET_DIAG_VENDOR_MODEL;
+const char Server::kVendorSwVersion[] = OPENTHREAD_CONFIG_NET_DIAG_VENDOR_SW_VERSION;
+
+//---------------------------------------------------------------------------------------------------------------------
+// Server
+
+Server::Server(Instance &aInstance)
     : InstanceLocator(aInstance)
 {
+    static_assert(sizeof(kVendorName) <= sizeof(VendorNameTlv::StringType), "VENDOR_NAME is too long");
+    static_assert(sizeof(kVendorModel) <= sizeof(VendorModelTlv::StringType), "VENDOR_MODEL is too long");
+    static_assert(sizeof(kVendorSwVersion) <= sizeof(VendorSwVersionTlv::StringType), "VENDOR_SW_VERSION is too long");
+
+#if OPENTHREAD_CONFIG_NET_DIAG_VENDOR_INFO_SET_API_ENABLE
+    memcpy(mVendorName, kVendorName, sizeof(kVendorName));
+    memcpy(mVendorModel, kVendorModel, sizeof(kVendorModel));
+    memcpy(mVendorSwVersion, kVendorSwVersion, sizeof(kVendorSwVersion));
+#endif
 }
 
-Error NetworkDiagnostic::SendDiagnosticGet(const Ip6::Address &aDestination,
-                                           const uint8_t       aTlvTypes[],
-                                           uint8_t             aCount,
-                                           GetCallback         aCallback,
-                                           void               *aContext)
+#if OPENTHREAD_CONFIG_NET_DIAG_VENDOR_INFO_SET_API_ENABLE
+
+Error Server::SetVendorName(const char *aVendorName)
 {
-    Error error;
+    return SetVendorString(mVendorName, sizeof(mVendorName), aVendorName);
+}
 
-    if (aDestination.IsMulticast())
-    {
-        error = SendCommand(kUriDiagnosticGetQuery, aDestination, aTlvTypes, aCount);
-    }
-    else
-    {
-        error = SendCommand(kUriDiagnosticGetRequest, aDestination, aTlvTypes, aCount, &HandleGetResponse, this);
-    }
+Error Server::SetVendorModel(const char *aVendorModel)
+{
+    return SetVendorString(mVendorModel, sizeof(mVendorModel), aVendorModel);
+}
 
-    SuccessOrExit(error);
+Error Server::SetVendorSwVersion(const char *aVendorSwVersion)
+{
+    return SetVendorString(mVendorSwVersion, sizeof(mVendorSwVersion), aVendorSwVersion);
+}
 
-    mGetCallback.Set(aCallback, aContext);
+Error Server::SetVendorString(char *aDestString, uint16_t kMaxSize, const char *aSrcString)
+{
+    Error    error = kErrorInvalidArgs;
+    uint16_t length;
+
+    VerifyOrExit(aSrcString != nullptr);
+
+    length = StringLength(aSrcString, kMaxSize);
+    VerifyOrExit(length < kMaxSize);
+
+    VerifyOrExit(IsValidUtf8String(aSrcString));
+
+    memcpy(aDestString, aSrcString, length + 1);
+    error = kErrorNone;
 
 exit:
     return error;
 }
 
-Error NetworkDiagnostic::SendCommand(Uri                   aUri,
-                                     const Ip6::Address   &aDestination,
-                                     const uint8_t         aTlvTypes[],
-                                     uint8_t               aCount,
-                                     Coap::ResponseHandler aHandler,
-                                     void                 *aContext)
-{
-    Error            error;
-    Coap::Message   *message = nullptr;
-    Tmf::MessageInfo messageInfo(GetInstance());
+#endif // OPENTHREAD_CONFIG_NET_DIAG_VENDOR_INFO_SET_API_ENABLE
 
-    switch (aUri)
-    {
-    case kUriDiagnosticGetQuery:
-        message = Get<Tmf::Agent>().NewNonConfirmablePostMessage(aUri);
-        break;
-
-    case kUriDiagnosticGetRequest:
-    case kUriDiagnosticReset:
-        message = Get<Tmf::Agent>().NewConfirmablePostMessage(aUri);
-        break;
-
-    default:
-        OT_ASSERT(false);
-    }
-
-    VerifyOrExit(message != nullptr, error = kErrorNoBufs);
-
-    if (aCount > 0)
-    {
-        SuccessOrExit(error = Tlv::Append<TypeListTlv>(*message, aTlvTypes, aCount));
-    }
-
-    PrepareMessageInfoForDest(aDestination, messageInfo);
-
-    SuccessOrExit(error = Get<Tmf::Agent>().SendMessage(*message, messageInfo, aHandler, aContext));
-
-    Log(kMessageSend, aUri, aDestination);
-
-exit:
-    FreeMessageOnError(message, error);
-    return error;
-}
-
-void NetworkDiagnostic::PrepareMessageInfoForDest(const Ip6::Address &aDestination,
-                                                  Tmf::MessageInfo   &aMessageInfo) const
+void Server::PrepareMessageInfoForDest(const Ip6::Address &aDestination, Tmf::MessageInfo &aMessageInfo) const
 {
     if (aDestination.IsMulticast())
     {
@@ -152,41 +134,7 @@
     aMessageInfo.SetPeerAddr(aDestination);
 }
 
-void NetworkDiagnostic::HandleGetResponse(void                *aContext,
-                                          otMessage           *aMessage,
-                                          const otMessageInfo *aMessageInfo,
-                                          Error                aResult)
-{
-    static_cast<NetworkDiagnostic *>(aContext)->HandleGetResponse(AsCoapMessagePtr(aMessage),
-                                                                  AsCoreTypePtr(aMessageInfo), aResult);
-}
-
-void NetworkDiagnostic::HandleGetResponse(Coap::Message *aMessage, const Ip6::MessageInfo *aMessageInfo, Error aResult)
-{
-    SuccessOrExit(aResult);
-    VerifyOrExit(aMessage->GetCode() == Coap::kCodeChanged, aResult = kErrorFailed);
-
-exit:
-    mGetCallback.InvokeIfSet(aResult, aMessage, aMessageInfo);
-}
-
-template <>
-void NetworkDiagnostic::HandleTmf<kUriDiagnosticGetAnswer>(Coap::Message          &aMessage,
-                                                           const Ip6::MessageInfo &aMessageInfo)
-{
-    VerifyOrExit(aMessage.IsConfirmablePostRequest());
-
-    Log(kMessageReceive, kUriDiagnosticGetAnswer, aMessageInfo.GetPeerAddr());
-
-    mGetCallback.InvokeIfSet(kErrorNone, &aMessage, &aMessageInfo);
-
-    IgnoreError(Get<Tmf::Agent>().SendEmptyAck(aMessage, aMessageInfo));
-
-exit:
-    return;
-}
-
-Error NetworkDiagnostic::AppendIp6AddressList(Message &aMessage)
+Error Server::AppendIp6AddressList(Message &aMessage)
 {
     Error    error = kErrorNone;
     uint16_t count = 0;
@@ -224,7 +172,7 @@
 }
 
 #if OPENTHREAD_FTD
-Error NetworkDiagnostic::AppendChildTable(Message &aMessage)
+Error Server::AppendChildTable(Message &aMessage)
 {
     Error    error = kErrorNone;
     uint16_t count;
@@ -276,7 +224,7 @@
 }
 #endif // OPENTHREAD_FTD
 
-Error NetworkDiagnostic::AppendMacCounters(Message &aMessage)
+Error Server::AppendMacCounters(Message &aMessage)
 {
     MacCountersTlv       tlv;
     const otMacCounters &counters = Get<Mac::Mac>().GetCounters();
@@ -298,7 +246,7 @@
     return tlv.AppendTo(aMessage);
 }
 
-Error NetworkDiagnostic::AppendRequestedTlvs(const Message &aRequest, Message &aResponse)
+Error Server::AppendRequestedTlvs(const Message &aRequest, Message &aResponse)
 {
     Error    error;
     uint16_t offset;
@@ -320,7 +268,7 @@
     return error;
 }
 
-Error NetworkDiagnostic::AppendDiagTlv(uint8_t aTlvType, Message &aMessage)
+Error Server::AppendDiagTlv(uint8_t aTlvType, Message &aMessage)
 {
     Error error = kErrorNone;
 
@@ -370,6 +318,22 @@
         error = AppendMacCounters(aMessage);
         break;
 
+    case Tlv::kVendorName:
+        error = Tlv::Append<VendorNameTlv>(aMessage, GetVendorName());
+        break;
+
+    case Tlv::kVendorModel:
+        error = Tlv::Append<VendorModelTlv>(aMessage, GetVendorModel());
+        break;
+
+    case Tlv::kVendorSwVersion:
+        error = Tlv::Append<VendorSwVersionTlv>(aMessage, GetVendorSwVersion());
+        break;
+
+    case Tlv::kThreadStackVersion:
+        error = Tlv::Append<ThreadStackVersionTlv>(aMessage, otGetVersionString());
+        break;
+
     case Tlv::kChannelPages:
     {
         ChannelPagesTlv tlv;
@@ -437,14 +401,16 @@
 }
 
 template <>
-void NetworkDiagnostic::HandleTmf<kUriDiagnosticGetQuery>(Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo)
+void Server::HandleTmf<kUriDiagnosticGetQuery>(Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo)
 {
     Error            error    = kErrorNone;
     Coap::Message   *response = nullptr;
     Tmf::MessageInfo responseInfo(GetInstance());
 
     VerifyOrExit(aMessage.IsPostRequest(), error = kErrorDrop);
-    Log(kMessageReceive, kUriDiagnosticGetQuery, aMessageInfo.GetPeerAddr());
+
+    LogInfo("Received %s from %s", UriToString<kUriDiagnosticGetQuery>(),
+            aMessageInfo.GetPeerAddr().ToString().AsCString());
 
     // DIAG_GET.qry may be sent as a confirmable request.
     if (aMessage.IsConfirmable())
@@ -465,14 +431,15 @@
 }
 
 template <>
-void NetworkDiagnostic::HandleTmf<kUriDiagnosticGetRequest>(Coap::Message          &aMessage,
-                                                            const Ip6::MessageInfo &aMessageInfo)
+void Server::HandleTmf<kUriDiagnosticGetRequest>(Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo)
 {
     Error          error    = kErrorNone;
     Coap::Message *response = nullptr;
 
     VerifyOrExit(aMessage.IsConfirmablePostRequest(), error = kErrorDrop);
-    Log(kMessageReceive, kUriDiagnosticGetRequest, aMessageInfo.GetPeerAddr());
+
+    LogInfo("Received %s from %s", UriToString<kUriDiagnosticGetRequest>(),
+            aMessageInfo.GetPeerAddr().ToString().AsCString());
 
     response = Get<Tmf::Agent>().NewResponseMessage(aMessage);
     VerifyOrExit(response != nullptr, error = kErrorNoBufs);
@@ -484,22 +451,16 @@
     FreeMessageOnError(response, error);
 }
 
-Error NetworkDiagnostic::SendDiagnosticReset(const Ip6::Address &aDestination,
-                                             const uint8_t       aTlvTypes[],
-                                             uint8_t             aCount)
-{
-    return SendCommand(kUriDiagnosticReset, aDestination, aTlvTypes, aCount);
-}
-
-template <>
-void NetworkDiagnostic::HandleTmf<kUriDiagnosticReset>(Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo)
+template <> void Server::HandleTmf<kUriDiagnosticReset>(Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo)
 {
     uint16_t offset = 0;
     uint8_t  type;
     Tlv      tlv;
 
     VerifyOrExit(aMessage.IsConfirmablePostRequest());
-    Log(kMessageReceive, kUriDiagnosticReset, aMessageInfo.GetPeerAddr());
+
+    LogInfo("Received %s from %s", UriToString<kUriDiagnosticReset>(),
+            aMessageInfo.GetPeerAddr().ToString().AsCString());
 
     SuccessOrExit(aMessage.Read(aMessage.GetOffset(), tlv));
 
@@ -528,6 +489,121 @@
     return;
 }
 
+#if OPENTHREAD_CONFIG_TMF_NETDIAG_CLIENT_ENABLE
+
+//---------------------------------------------------------------------------------------------------------------------
+// Client
+
+Client::Client(Instance &aInstance)
+    : InstanceLocator(aInstance)
+{
+}
+
+Error Client::SendDiagnosticGet(const Ip6::Address &aDestination,
+                                const uint8_t       aTlvTypes[],
+                                uint8_t             aCount,
+                                GetCallback         aCallback,
+                                void               *aContext)
+{
+    Error error;
+
+    if (aDestination.IsMulticast())
+    {
+        error = SendCommand(kUriDiagnosticGetQuery, aDestination, aTlvTypes, aCount);
+    }
+    else
+    {
+        error = SendCommand(kUriDiagnosticGetRequest, aDestination, aTlvTypes, aCount, &HandleGetResponse, this);
+    }
+
+    SuccessOrExit(error);
+
+    mGetCallback.Set(aCallback, aContext);
+
+exit:
+    return error;
+}
+
+Error Client::SendCommand(Uri                   aUri,
+                          const Ip6::Address   &aDestination,
+                          const uint8_t         aTlvTypes[],
+                          uint8_t               aCount,
+                          Coap::ResponseHandler aHandler,
+                          void                 *aContext)
+{
+    Error            error;
+    Coap::Message   *message = nullptr;
+    Tmf::MessageInfo messageInfo(GetInstance());
+
+    switch (aUri)
+    {
+    case kUriDiagnosticGetQuery:
+        message = Get<Tmf::Agent>().NewNonConfirmablePostMessage(aUri);
+        break;
+
+    case kUriDiagnosticGetRequest:
+    case kUriDiagnosticReset:
+        message = Get<Tmf::Agent>().NewConfirmablePostMessage(aUri);
+        break;
+
+    default:
+        OT_ASSERT(false);
+    }
+
+    VerifyOrExit(message != nullptr, error = kErrorNoBufs);
+
+    if (aCount > 0)
+    {
+        SuccessOrExit(error = Tlv::Append<TypeListTlv>(*message, aTlvTypes, aCount));
+    }
+
+    Get<Server>().PrepareMessageInfoForDest(aDestination, messageInfo);
+
+    SuccessOrExit(error = Get<Tmf::Agent>().SendMessage(*message, messageInfo, aHandler, aContext));
+
+    LogInfo("Sent %s to %s", UriToString(aUri), aDestination.ToString().AsCString());
+
+exit:
+    FreeMessageOnError(message, error);
+    return error;
+}
+
+void Client::HandleGetResponse(void *aContext, otMessage *aMessage, const otMessageInfo *aMessageInfo, Error aResult)
+{
+    static_cast<Client *>(aContext)->HandleGetResponse(AsCoapMessagePtr(aMessage), AsCoreTypePtr(aMessageInfo),
+                                                       aResult);
+}
+
+void Client::HandleGetResponse(Coap::Message *aMessage, const Ip6::MessageInfo *aMessageInfo, Error aResult)
+{
+    SuccessOrExit(aResult);
+    VerifyOrExit(aMessage->GetCode() == Coap::kCodeChanged, aResult = kErrorFailed);
+
+exit:
+    mGetCallback.InvokeIfSet(aResult, aMessage, aMessageInfo);
+}
+
+template <>
+void Client::HandleTmf<kUriDiagnosticGetAnswer>(Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo)
+{
+    VerifyOrExit(aMessage.IsConfirmablePostRequest());
+
+    LogInfo("Received %s from %s", ot::UriToString<kUriDiagnosticGetAnswer>(),
+            aMessageInfo.GetPeerAddr().ToString().AsCString());
+
+    mGetCallback.InvokeIfSet(kErrorNone, &aMessage, &aMessageInfo);
+
+    IgnoreError(Get<Tmf::Agent>().SendEmptyAck(aMessage, aMessageInfo));
+
+exit:
+    return;
+}
+
+Error Client::SendDiagnosticReset(const Ip6::Address &aDestination, const uint8_t aTlvTypes[], uint8_t aCount)
+{
+    return SendCommand(kUriDiagnosticReset, aDestination, aTlvTypes, aCount);
+}
+
 static void ParseRoute(const RouteTlv &aRouteTlv, otNetworkDiagRoute &aNetworkDiagRoute)
 {
     uint8_t routeCount = 0;
@@ -561,9 +637,9 @@
     aMacCounters.mIfOutDiscards      = aMacCountersTlv.GetIfOutDiscards();
 }
 
-Error NetworkDiagnostic::GetNextDiagTlv(const Coap::Message &aMessage, Iterator &aIterator, TlvInfo &aTlvInfo)
+Error Client::GetNextDiagTlv(const Coap::Message &aMessage, Iterator &aIterator, TlvInfo &aTlvInfo)
 {
-    Error    error  = kErrorNotFound;
+    Error    error;
     uint16_t offset = (aIterator == 0) ? aMessage.GetOffset() : aIterator;
 
     while (offset < aMessage.GetLength())
@@ -752,6 +828,23 @@
             SuccessOrExit(error = Tlv::Read<VersionTlv>(aMessage, offset, aTlvInfo.mData.mVersion));
             break;
 
+        case Tlv::kVendorName:
+            SuccessOrExit(error = Tlv::Read<VendorNameTlv>(aMessage, offset, aTlvInfo.mData.mVendorName));
+            break;
+
+        case Tlv::kVendorModel:
+            SuccessOrExit(error = Tlv::Read<VendorModelTlv>(aMessage, offset, aTlvInfo.mData.mVendorModel));
+            break;
+
+        case Tlv::kVendorSwVersion:
+            SuccessOrExit(error = Tlv::Read<VendorSwVersionTlv>(aMessage, offset, aTlvInfo.mData.mVendorSwVersion));
+            break;
+
+        case Tlv::kThreadStackVersion:
+            SuccessOrExit(error =
+                              Tlv::Read<ThreadStackVersionTlv>(aMessage, offset, aTlvInfo.mData.mThreadStackVersion));
+            break;
+
         default:
             // Skip unrecognized TLVs.
             skipTlv = true;
@@ -765,33 +858,33 @@
             // Exit if a TLV is recognized and parsed successfully.
             aTlvInfo.mType = tlv.GetType();
             aIterator      = offset;
+            error          = kErrorNone;
             ExitNow();
         }
     }
 
+    error = kErrorNotFound;
+
 exit:
     return error;
 }
 
 #if OT_SHOULD_LOG_AT(OT_LOG_LEVEL_INFO)
 
-const char *NetworkDiagnostic::UriToString(Uri aUri)
+const char *Client::UriToString(Uri aUri)
 {
     const char *str = "";
 
     switch (aUri)
     {
     case kUriDiagnosticGetQuery:
-        str = "DiagGetQuery";
+        str = ot::UriToString<kUriDiagnosticGetQuery>();
         break;
     case kUriDiagnosticGetRequest:
-        str = "DiagGetRequest";
+        str = ot::UriToString<kUriDiagnosticGetRequest>();
         break;
     case kUriDiagnosticReset:
-        str = "DiagReset";
-        break;
-    case kUriDiagnosticGetAnswer:
-        str = "DiagGetAnswer";
+        str = ot::UriToString<kUriDiagnosticReset>();
         break;
     default:
         break;
@@ -800,29 +893,10 @@
     return str;
 }
 
-void NetworkDiagnostic::Log(Action aAction, Uri aUri, const Ip6::Address &aIp6Address) const
-{
-    static const char *const kActionStrings[] = {
-        "Sent",     // (0) kMessageSend
-        "Received", // (1) kMessageReceive
-    };
-
-    static const char *const kActionPrepositionStrings[] = {
-        "to",   // (0) kMessageSend
-        "from", // (1) kMessageReceive
-    };
-
-    static_assert(kMessageSend == 0, "kMessageSend value is incorrect");
-    static_assert(kMessageReceive == 1, "kMessageReceive value is incorrect");
-
-    LogInfo("%s %s %s %s", kActionStrings[aAction], UriToString(aUri), kActionPrepositionStrings[aAction],
-            aIp6Address.ToString().AsCString());
-}
-
 #endif // #if OT_SHOULD_LOG_AT(OT_LOG_LEVEL_INFO)
 
+#endif // OPENTHREAD_CONFIG_TMF_NETDIAG_CLIENT_ENABLE
+
 } // namespace NetworkDiagnostic
 
 } // namespace ot
-
-#endif // OPENTHREAD_FTD || OPENTHREAD_CONFIG_TMF_NETWORK_DIAG_MTD_ENABLE
diff --git a/src/core/thread/network_diagnostic.hpp b/src/core/thread/network_diagnostic.hpp
index 4025944..4026aa3 100644
--- a/src/core/thread/network_diagnostic.hpp
+++ b/src/core/thread/network_diagnostic.hpp
@@ -36,8 +36,6 @@
 
 #include "openthread-core-config.h"
 
-#if OPENTHREAD_FTD || OPENTHREAD_CONFIG_TMF_NETWORK_DIAG_MTD_ENABLE
-
 #include <openthread/netdiag.h>
 
 #include "common/callback.hpp"
@@ -61,11 +59,126 @@
  * @{
  */
 
+class Client;
+
 /**
- * This class implements the Network Diagnostic processing.
+ * This class implements the Network Diagnostic server responding to requests.
  *
  */
-class NetworkDiagnostic : public InstanceLocator, private NonCopyable
+class Server : public InstanceLocator, private NonCopyable
+{
+    friend class Tmf::Agent;
+    friend class Client;
+
+public:
+    /**
+     * This constructor initializes the Server.
+     *
+     * @param[in] aInstance   The OpenThread instance.
+     *
+     */
+    explicit Server(Instance &aInstance);
+
+#if OPENTHREAD_CONFIG_NET_DIAG_VENDOR_INFO_SET_API_ENABLE
+    /**
+     * This method returns the vendor name string.
+     *
+     * @returns The vendor name string.
+     *
+     */
+    const char *GetVendorName(void) const { return mVendorName; }
+
+    /**
+     * This method sets the vendor name string.
+     *
+     * @param[in] aVendorName     The vendor name string.
+     *
+     * @retval kErrorNone         Successfully set the vendor name.
+     * @retval kErrorInvalidArgs  @p aVendorName is not valid (too long or not UTF8).
+     *
+     */
+    Error SetVendorName(const char *aVendorName);
+
+    /**
+     * This method returns the vendor model string.
+     *
+     * @returns The vendor model string.
+     *
+     */
+    const char *GetVendorModel(void) const { return mVendorModel; }
+
+    /**
+     * This method sets the vendor model string.
+     *
+     * @param[in] aVendorModel     The vendor model string.
+     *
+     * @retval kErrorNone         Successfully set the vendor model.
+     * @retval kErrorInvalidArgs  @p aVendorModel is not valid (too long or not UTF8).
+     *
+     */
+    Error SetVendorModel(const char *aVendorModel);
+
+    /**
+     * This method returns the vendor software version string.
+     *
+     * @returns The vendor software version string.
+     *
+     */
+    const char *GetVendorSwVersion(void) const { return mVendorSwVersion; }
+
+    /**
+     * This method sets the vendor sw version string
+     *
+     * @param[in] aVendorSwVersion     The vendor sw version string.
+     *
+     * @retval kErrorNone         Successfully set the vendor sw version.
+     * @retval kErrorInvalidArgs  @p aVendorSwVersion is not valid (too long or not UTF8).
+     *
+     */
+    Error SetVendorSwVersion(const char *aVendorSwVersion);
+
+#else
+    const char *GetVendorName(void) const { return kVendorName; }
+    const char *GetVendorModel(void) const { return kVendorModel; }
+    const char *GetVendorSwVersion(void) const { return kVendorSwVersion; }
+#endif // OPENTHREAD_CONFIG_NET_DIAG_VENDOR_INFO_SET_API_ENABLE
+
+private:
+    static constexpr uint16_t kMaxChildEntries = 398;
+
+    static const char kVendorName[];
+    static const char kVendorModel[];
+    static const char kVendorSwVersion[];
+
+    Error AppendDiagTlv(uint8_t aTlvType, Message &aMessage);
+    Error AppendIp6AddressList(Message &aMessage);
+    Error AppendMacCounters(Message &aMessage);
+    Error AppendChildTable(Message &aMessage);
+    Error AppendRequestedTlvs(const Message &aRequest, Message &aResponse);
+    void  PrepareMessageInfoForDest(const Ip6::Address &aDestination, Tmf::MessageInfo &aMessageInfo) const;
+
+    template <Uri kUri> void HandleTmf(Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo);
+
+#if OPENTHREAD_CONFIG_NET_DIAG_VENDOR_INFO_SET_API_ENABLE
+    Error SetVendorString(char *aDestString, uint16_t kMaxSize, const char *aSrcString);
+
+    VendorNameTlv::StringType      mVendorName;
+    VendorModelTlv::StringType     mVendorModel;
+    VendorSwVersionTlv::StringType mVendorSwVersion;
+#endif
+};
+
+DeclareTmfHandler(Server, kUriDiagnosticGetRequest);
+DeclareTmfHandler(Server, kUriDiagnosticGetQuery);
+DeclareTmfHandler(Server, kUriDiagnosticGetAnswer);
+
+#if OPENTHREAD_CONFIG_TMF_NETDIAG_CLIENT_ENABLE
+
+/**
+ * This class implements the Network Diagnostic client sending requests and queries.
+ *
+ */
+class Client : public InstanceLocator, private NonCopyable
 {
     friend class Tmf::Agent;
 
@@ -78,10 +191,12 @@
     static constexpr Iterator kIteratorInit = OT_NETWORK_DIAGNOSTIC_ITERATOR_INIT; ///< Initializer for Iterator.
 
     /**
-     * This constructor initializes the object.
+     * This constructor initializes the Client.
+     *
+     * @param[in] aInstance   The OpenThread instance.
      *
      */
-    explicit NetworkDiagnostic(Instance &aInstance);
+    explicit Client(Instance &aInstance);
 
     /**
      * This method sends Diagnostic Get request. If the @p aDestination is of multicast type, the DIAG_GET.qry
@@ -125,14 +240,6 @@
     static Error GetNextDiagTlv(const Coap::Message &aMessage, Iterator &aIterator, TlvInfo &aTlvInfo);
 
 private:
-    static constexpr uint16_t kMaxChildEntries = 398;
-
-    enum Action : uint8_t
-    {
-        kMessageSend,
-        kMessageReceive,
-    };
-
     Error SendCommand(Uri                   aUri,
                       const Ip6::Address   &aDestination,
                       const uint8_t         aTlvTypes[],
@@ -140,13 +247,6 @@
                       Coap::ResponseHandler aHandler = nullptr,
                       void                 *aContext = nullptr);
 
-    Error AppendDiagTlv(uint8_t aTlvType, Message &aMessage);
-    Error AppendIp6AddressList(Message &aMessage);
-    Error AppendMacCounters(Message &aMessage);
-    Error AppendChildTable(Message &aMessage);
-    Error AppendRequestedTlvs(const Message &aRequest, Message &aResponse);
-    void  PrepareMessageInfoForDest(const Ip6::Address &aDestination, Tmf::MessageInfo &aMessageInfo) const;
-
     static void HandleGetResponse(void                *aContext,
                                   otMessage           *aMessage,
                                   const otMessageInfo *aMessageInfo,
@@ -157,18 +257,14 @@
 
 #if OT_SHOULD_LOG_AT(OT_LOG_LEVEL_INFO)
     static const char *UriToString(Uri aUri);
-    void               Log(Action aAction, Uri aUri, const Ip6::Address &aIp6Address) const;
-#else
-    void Log(Action, Uri, const Ip6::Address &) const {}
 #endif
 
     Callback<GetCallback> mGetCallback;
 };
 
-DeclareTmfHandler(NetworkDiagnostic, kUriDiagnosticGetRequest);
-DeclareTmfHandler(NetworkDiagnostic, kUriDiagnosticGetQuery);
-DeclareTmfHandler(NetworkDiagnostic, kUriDiagnosticGetAnswer);
-DeclareTmfHandler(NetworkDiagnostic, kUriDiagnosticReset);
+DeclareTmfHandler(Client, kUriDiagnosticReset);
+
+#endif // OPENTHREAD_CONFIG_TMF_NETDIAG_CLIENT_ENABLE
 
 /**
  * @}
@@ -177,6 +273,4 @@
 
 } // namespace ot
 
-#endif // OPENTHREAD_FTD || OPENTHREAD_CONFIG_TMF_NETWORK_DIAG_MTD_ENABLE
-
 #endif // NETWORK_DIAGNOSTIC_HPP_
diff --git a/src/core/thread/network_diagnostic_tlvs.hpp b/src/core/thread/network_diagnostic_tlvs.hpp
index 6d1fd60..c46298c 100644
--- a/src/core/thread/network_diagnostic_tlvs.hpp
+++ b/src/core/thread/network_diagnostic_tlvs.hpp
@@ -69,26 +69,54 @@
      */
     enum Type : uint8_t
     {
-        kExtMacAddress   = OT_NETWORK_DIAGNOSTIC_TLV_EXT_ADDRESS,
-        kAddress16       = OT_NETWORK_DIAGNOSTIC_TLV_SHORT_ADDRESS,
-        kMode            = OT_NETWORK_DIAGNOSTIC_TLV_MODE,
-        kTimeout         = OT_NETWORK_DIAGNOSTIC_TLV_TIMEOUT,
-        kConnectivity    = OT_NETWORK_DIAGNOSTIC_TLV_CONNECTIVITY,
-        kRoute           = OT_NETWORK_DIAGNOSTIC_TLV_ROUTE,
-        kLeaderData      = OT_NETWORK_DIAGNOSTIC_TLV_LEADER_DATA,
-        kNetworkData     = OT_NETWORK_DIAGNOSTIC_TLV_NETWORK_DATA,
-        kIp6AddressList  = OT_NETWORK_DIAGNOSTIC_TLV_IP6_ADDR_LIST,
-        kMacCounters     = OT_NETWORK_DIAGNOSTIC_TLV_MAC_COUNTERS,
-        kBatteryLevel    = OT_NETWORK_DIAGNOSTIC_TLV_BATTERY_LEVEL,
-        kSupplyVoltage   = OT_NETWORK_DIAGNOSTIC_TLV_SUPPLY_VOLTAGE,
-        kChildTable      = OT_NETWORK_DIAGNOSTIC_TLV_CHILD_TABLE,
-        kChannelPages    = OT_NETWORK_DIAGNOSTIC_TLV_CHANNEL_PAGES,
-        kTypeList        = OT_NETWORK_DIAGNOSTIC_TLV_TYPE_LIST,
-        kMaxChildTimeout = OT_NETWORK_DIAGNOSTIC_TLV_MAX_CHILD_TIMEOUT,
-        kVersion         = OT_NETWORK_DIAGNOSTIC_TLV_VERSION,
+        kExtMacAddress      = OT_NETWORK_DIAGNOSTIC_TLV_EXT_ADDRESS,
+        kAddress16          = OT_NETWORK_DIAGNOSTIC_TLV_SHORT_ADDRESS,
+        kMode               = OT_NETWORK_DIAGNOSTIC_TLV_MODE,
+        kTimeout            = OT_NETWORK_DIAGNOSTIC_TLV_TIMEOUT,
+        kConnectivity       = OT_NETWORK_DIAGNOSTIC_TLV_CONNECTIVITY,
+        kRoute              = OT_NETWORK_DIAGNOSTIC_TLV_ROUTE,
+        kLeaderData         = OT_NETWORK_DIAGNOSTIC_TLV_LEADER_DATA,
+        kNetworkData        = OT_NETWORK_DIAGNOSTIC_TLV_NETWORK_DATA,
+        kIp6AddressList     = OT_NETWORK_DIAGNOSTIC_TLV_IP6_ADDR_LIST,
+        kMacCounters        = OT_NETWORK_DIAGNOSTIC_TLV_MAC_COUNTERS,
+        kBatteryLevel       = OT_NETWORK_DIAGNOSTIC_TLV_BATTERY_LEVEL,
+        kSupplyVoltage      = OT_NETWORK_DIAGNOSTIC_TLV_SUPPLY_VOLTAGE,
+        kChildTable         = OT_NETWORK_DIAGNOSTIC_TLV_CHILD_TABLE,
+        kChannelPages       = OT_NETWORK_DIAGNOSTIC_TLV_CHANNEL_PAGES,
+        kTypeList           = OT_NETWORK_DIAGNOSTIC_TLV_TYPE_LIST,
+        kMaxChildTimeout    = OT_NETWORK_DIAGNOSTIC_TLV_MAX_CHILD_TIMEOUT,
+        kVersion            = OT_NETWORK_DIAGNOSTIC_TLV_VERSION,
+        kVendorName         = OT_NETWORK_DIAGNOSTIC_TLV_VENDOR_NAME,
+        kVendorModel        = OT_NETWORK_DIAGNOSTIC_TLV_VENDOR_MODEL,
+        kVendorSwVersion    = OT_NETWORK_DIAGNOSTIC_TLV_VENDOR_SW_VERSION,
+        kThreadStackVersion = OT_NETWORK_DIAGNOSTIC_TLV_THREAD_STACK_VERSION,
     };
 
     /**
+     * Maximum length of Vendor Name TLV.
+     *
+     */
+    static constexpr uint8_t kMaxVendorNameLength = OT_NETWORK_DIAGNOSTIC_MAX_VENDOR_NAME_TLV_LENGTH;
+
+    /**
+     * Maximum length of Vendor Model TLV.
+     *
+     */
+    static constexpr uint8_t kMaxVendorModelLength = OT_NETWORK_DIAGNOSTIC_MAX_VENDOR_MODEL_TLV_LENGTH;
+
+    /**
+     * Maximum length of Vendor SW Version TLV.
+     *
+     */
+    static constexpr uint8_t kMaxVendorSwVersionLength = OT_NETWORK_DIAGNOSTIC_MAX_VENDOR_SW_VERSION_TLV_LENGTH;
+
+    /**
+     * Maximum length of Vendor SW Version TLV.
+     *
+     */
+    static constexpr uint8_t kMaxThreadStackVersionLength = OT_NETWORK_DIAGNOSTIC_MAX_THREAD_STACK_VERSION_TLV_LENGTH;
+
+    /**
      * This method returns the Type value.
      *
      * @returns The Type value.
@@ -172,6 +200,30 @@
  */
 typedef UintTlvInfo<Tlv::kVersion, uint16_t> VersionTlv;
 
+/**
+ * This class defines Vendor Name TLV constants and types.
+ *
+ */
+typedef StringTlvInfo<Tlv::kVendorName, Tlv::kMaxVendorNameLength> VendorNameTlv;
+
+/**
+ * This class defines Vendor Model TLV constants and types.
+ *
+ */
+typedef StringTlvInfo<Tlv::kVendorModel, Tlv::kMaxVendorModelLength> VendorModelTlv;
+
+/**
+ * This class defines Vendor SW Version TLV constants and types.
+ *
+ */
+typedef StringTlvInfo<Tlv::kVendorSwVersion, Tlv::kMaxVendorSwVersionLength> VendorSwVersionTlv;
+
+/**
+ * This class defines Thread Stack Version TLV constants and types.
+ *
+ */
+typedef StringTlvInfo<Tlv::kThreadStackVersion, Tlv::kMaxThreadStackVersionLength> ThreadStackVersionTlv;
+
 typedef otNetworkDiagConnectivity Connectivity; ///< Network Diagnostic Connectivity value.
 
 /**
diff --git a/src/core/thread/panid_query_server.cpp b/src/core/thread/panid_query_server.cpp
index ae56cf6..01df10a 100644
--- a/src/core/thread/panid_query_server.cpp
+++ b/src/core/thread/panid_query_server.cpp
@@ -60,9 +60,8 @@
 template <>
 void PanIdQueryServer::HandleTmf<kUriPanIdQuery>(Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo)
 {
-    uint16_t         panId;
-    Ip6::MessageInfo responseInfo(aMessageInfo);
-    uint32_t         mask;
+    uint16_t panId;
+    uint32_t mask;
 
     VerifyOrExit(aMessage.IsPostRequest());
     VerifyOrExit((mask = MeshCoP::ChannelMaskTlv::GetChannelMask(aMessage)) != 0);
@@ -76,8 +75,8 @@
 
     if (aMessage.IsConfirmable() && !aMessageInfo.GetSockAddr().IsMulticast())
     {
-        SuccessOrExit(Get<Tmf::Agent>().SendEmptyAck(aMessage, responseInfo));
-        LogInfo("sent panid query response");
+        SuccessOrExit(Get<Tmf::Agent>().SendEmptyAck(aMessage, aMessageInfo));
+        LogInfo("Sent %s ack", UriToString<kUriPanIdQuery>());
     }
 
 exit:
@@ -124,7 +123,7 @@
 
     SuccessOrExit(error = Get<Tmf::Agent>().SendMessage(*message, messageInfo));
 
-    LogInfo("sent panid conflict");
+    LogInfo("Sent %s", UriToString<kUriPanIdConflict>());
 
 exit:
     FreeMessageOnError(message, error);
diff --git a/src/core/thread/radio_selector.cpp b/src/core/thread/radio_selector.cpp
index 7153f84..965734a 100644
--- a/src/core/thread/radio_selector.cpp
+++ b/src/core/thread/radio_selector.cpp
@@ -85,28 +85,28 @@
 LogLevel RadioSelector::UpdatePreference(Neighbor &aNeighbor, Mac::RadioType aRadioType, int16_t aDifference)
 {
     uint8_t old        = aNeighbor.GetRadioPreference(aRadioType);
-    int16_t preferecne = static_cast<int16_t>(old);
+    int16_t preference = static_cast<int16_t>(old);
 
-    preferecne += aDifference;
+    preference += aDifference;
 
-    if (preferecne > kMaxPreference)
+    if (preference > kMaxPreference)
     {
-        preferecne = kMaxPreference;
+        preference = kMaxPreference;
     }
 
-    if (preferecne < kMinPreference)
+    if (preference < kMinPreference)
     {
-        preferecne = kMinPreference;
+        preference = kMinPreference;
     }
 
-    aNeighbor.SetRadioPreference(aRadioType, static_cast<uint8_t>(preferecne));
+    aNeighbor.SetRadioPreference(aRadioType, static_cast<uint8_t>(preference));
 
     // We check whether the update to the preference value caused it
     // to cross the threshold `kHighPreference`. Based on this we
     // return a suggested log level. If there is cross, suggest info
     // log level, otherwise debug log level.
 
-    return ((old >= kHighPreference) != (preferecne >= kHighPreference)) ? kLogLevelInfo : kLogLevelDebg;
+    return ((old >= kHighPreference) != (preference >= kHighPreference)) ? kLogLevelInfo : kLogLevelDebg;
 }
 
 void RadioSelector::UpdateOnReceive(Neighbor &aNeighbor, Mac::RadioType aRadioType, bool aIsDuplicate)
diff --git a/src/core/thread/router_table.cpp b/src/core/thread/router_table.cpp
index 8faced1..407f511 100644
--- a/src/core/thread/router_table.cpp
+++ b/src/core/thread/router_table.cpp
@@ -216,7 +216,7 @@
     mRouterIdSequence++;
     mRouterIdSequenceLastUpdated = TimerMilli::GetNow();
 
-    Get<AddressResolver>().Remove(aRouterId);
+    Get<AddressResolver>().RemoveEntriesForRouterId(aRouterId);
     Get<NetworkData::Leader>().RemoveBorderRouter(Mle::Rloc16FromRouterId(aRouterId),
                                                   NetworkData::Leader::kMatchModeRouterId);
     Get<Mle::MleRouter>().ResetAdvertiseInterval();
@@ -255,7 +255,7 @@
         Get<Mle::MleRouter>().ResetAdvertiseInterval();
 
         // Clear all EID-to-RLOC entries associated with the router.
-        Get<AddressResolver>().Remove(aRouter.GetRouterId());
+        Get<AddressResolver>().RemoveEntriesForRouterId(aRouter.GetRouterId());
     }
 }
 
diff --git a/src/core/thread/tmf.cpp b/src/core/thread/tmf.cpp
index 3f35c57..583f6e2 100644
--- a/src/core/thread/tmf.cpp
+++ b/src/core/thread/tmf.cpp
@@ -34,6 +34,7 @@
 #include "thread/tmf.hpp"
 
 #include "common/locator_getters.hpp"
+#include "net/ip6_types.hpp"
 
 namespace ot {
 namespace Tmf {
@@ -164,11 +165,12 @@
         Case(kUriAnycastLocate, AnycastLocator);
 #endif
 
-#if OPENTHREAD_FTD || OPENTHREAD_CONFIG_TMF_NETWORK_DIAG_MTD_ENABLE
-        Case(kUriDiagnosticGetRequest, NetworkDiagnostic::NetworkDiagnostic);
-        Case(kUriDiagnosticGetQuery, NetworkDiagnostic::NetworkDiagnostic);
-        Case(kUriDiagnosticGetAnswer, NetworkDiagnostic::NetworkDiagnostic);
-        Case(kUriDiagnosticReset, NetworkDiagnostic::NetworkDiagnostic);
+        Case(kUriDiagnosticGetRequest, NetworkDiagnostic::Server);
+        Case(kUriDiagnosticGetQuery, NetworkDiagnostic::Server);
+        Case(kUriDiagnosticReset, NetworkDiagnostic::Server);
+
+#if OPENTHREAD_CONFIG_TMF_NETDIAG_CLIENT_ENABLE
+        Case(kUriDiagnosticGetAnswer, NetworkDiagnostic::Client);
 #endif
 
 #if OPENTHREAD_FTD && OPENTHREAD_CONFIG_BACKBONE_ROUTER_ENABLE
@@ -222,6 +224,53 @@
     return isTmf;
 }
 
+uint8_t Agent::PriorityToDscp(Message::Priority aPriority)
+{
+    uint8_t dscp = Ip6::kDscpTmfNormalPriority;
+
+    switch (aPriority)
+    {
+    case Message::kPriorityNet:
+        dscp = Ip6::kDscpTmfNetPriority;
+        break;
+
+    case Message::kPriorityHigh:
+    case Message::kPriorityNormal:
+        break;
+
+    case Message::kPriorityLow:
+        dscp = Ip6::kDscpTmfLowPriority;
+        break;
+    }
+
+    return dscp;
+}
+
+Message::Priority Agent::DscpToPriority(uint8_t aDscp)
+{
+    Message::Priority priority = Message::kPriorityNet;
+
+    // If the sender does not use TMF specific DSCP value, we use
+    // `kPriorityNet`. This ensures that senders that do not use the
+    // new value (older firmware) experience the same behavior as
+    // before where all TMF message were treated as `kPriorityNet`.
+
+    switch (aDscp)
+    {
+    case Ip6::kDscpTmfNetPriority:
+    default:
+        break;
+    case Ip6::kDscpTmfNormalPriority:
+        priority = Message::kPriorityNormal;
+        break;
+    case Ip6::kDscpTmfLowPriority:
+        priority = Message::kPriorityLow;
+        break;
+    }
+
+    return priority;
+}
+
 #if OPENTHREAD_CONFIG_DTLS_ENABLE
 
 SecureAgent::SecureAgent(Instance &aInstance)
diff --git a/src/core/thread/tmf.hpp b/src/core/thread/tmf.hpp
index ecb3894..02a1aa3 100644
--- a/src/core/thread/tmf.hpp
+++ b/src/core/thread/tmf.hpp
@@ -182,6 +182,26 @@
      */
     bool IsTmfMessage(const Ip6::Address &aSourceAddress, const Ip6::Address &aDestAddress, uint16_t aDestPort) const;
 
+    /**
+     * This static method converts a TMF message priority to IPv6 header DSCP value.
+     *
+     * @param[in] aPriority  The message priority to convert.
+     *
+     * @returns The DSCP value corresponding to @p aPriority.
+     *
+     */
+    static uint8_t PriorityToDscp(Message::Priority aPriority);
+
+    /**
+     * This static method converts a IPv6 header DSCP value to message priority for TMF message.
+     *
+     * @param[in] aDscp      The IPv6 header DSCP value in a TMF message.
+     *
+     * @returns The message priority corresponding to the @p aDscp.
+     *
+     */
+    static Message::Priority DscpToPriority(uint8_t aDscp);
+
 private:
     template <Uri kUri> void HandleTmf(Message &aMessage, const Ip6::MessageInfo &aMessageInfo);
 
diff --git a/src/core/thread/topology.cpp b/src/core/thread/topology.cpp
index 8b6e658..ebb6c94 100644
--- a/src/core/thread/topology.cpp
+++ b/src/core/thread/topology.cpp
@@ -42,6 +42,29 @@
 
 namespace ot {
 
+void Neighbor::SetState(State aState)
+{
+    VerifyOrExit(mState != aState);
+    mState = static_cast<uint8_t>(aState);
+
+#if OPENTHREAD_CONFIG_UPTIME_ENABLE
+    if (mState == kStateValid)
+    {
+        mConnectionStart = Uptime::MsecToSec(Get<Uptime>().GetUptime());
+    }
+#endif
+
+exit:
+    return;
+}
+
+#if OPENTHREAD_CONFIG_UPTIME_ENABLE
+uint32_t Neighbor::GetConnectionTime(void) const
+{
+    return IsStateValid() ? Uptime::MsecToSec(Get<Uptime>().GetUptime()) - mConnectionStart : 0;
+}
+#endif
+
 bool Neighbor::AddressMatcher::Matches(const Neighbor &aNeighbor) const
 {
     bool matches = false;
@@ -83,6 +106,9 @@
     mFullThreadDevice = aNeighbor.IsFullThreadDevice();
     mFullNetworkData  = (aNeighbor.GetNetworkDataType() == NetworkData::kFullSet);
     mVersion          = aNeighbor.GetVersion();
+#if OPENTHREAD_CONFIG_UPTIME_ENABLE
+    mConnectionTime = aNeighbor.GetConnectionTime();
+#endif
 }
 
 void Neighbor::Init(Instance &aInstance)
@@ -176,7 +202,7 @@
         Random::Crypto::FillBuffer(mValidPending.mPending.mChallenge, sizeof(mValidPending.mPending.mChallenge)));
 }
 
-#if OPENTHREAD_CONFIG_MLE_LINK_METRICS_INITIATOR_ENABLE || OPENTHREAD_CONFIG_MLE_LINK_METRICS_SUBJECT_ENABLE
+#if OPENTHREAD_CONFIG_MLE_LINK_METRICS_SUBJECT_ENABLE
 void Neighbor::AggregateLinkMetrics(uint8_t aSeriesId, uint8_t aFrameType, uint8_t aLqi, int8_t aRss)
 {
     for (LinkMetrics::SeriesInfo &entry : mLinkMetricsSeriesInfoList)
@@ -208,10 +234,10 @@
     while (!mLinkMetricsSeriesInfoList.IsEmpty())
     {
         LinkMetrics::SeriesInfo *seriesInfo = mLinkMetricsSeriesInfoList.Pop();
-        Get<LinkMetrics::LinkMetrics>().mSeriesInfoPool.Free(*seriesInfo);
+        Get<LinkMetrics::Subject>().Free(*seriesInfo);
     }
 }
-#endif // OPENTHREAD_CONFIG_MLE_LINK_METRICS_INITIATOR_ENABLE || OPENTHREAD_CONFIG_MLE_LINK_METRICS_SUBJECT_ENABLE
+#endif // OPENTHREAD_CONFIG_MLE_LINK_METRICS_SUBJECT_ENABLE
 
 const char *Neighbor::StateToString(State aState)
 {
@@ -266,6 +292,9 @@
 #else
     mIsCslSynced = false;
 #endif
+#if OPENTHREAD_CONFIG_UPTIME_ENABLE
+    mConnectionTime = aChild.GetConnectionTime();
+#endif
 }
 
 const Ip6::Address *Child::AddressIterator::GetAddress(void) const
diff --git a/src/core/thread/topology.hpp b/src/core/thread/topology.hpp
index dcebedd..2f89fae 100644
--- a/src/core/thread/topology.hpp
+++ b/src/core/thread/topology.hpp
@@ -47,6 +47,7 @@
 #include "common/random.hpp"
 #include "common/serial_number.hpp"
 #include "common/timer.hpp"
+#include "common/uptime.hpp"
 #include "mac/mac_types.hpp"
 #include "net/ip6.hpp"
 #include "radio/radio.hpp"
@@ -224,7 +225,7 @@
      * @param[in]  aState  The state value.
      *
      */
-    void SetState(State aState) { mState = static_cast<uint8_t>(aState); }
+    void SetState(State aState);
 
     /**
      * This method indicates whether the neighbor is in the Invalid state.
@@ -542,7 +543,7 @@
      * This method MUST be used only when the tag is set (and not cleared). Otherwise its behavior is undefined.
      *
      * The tag value compassion follows the Serial Number Arithmetic logic from RFC-1982. It is semantically equivalent
-     * to `LastRxFragementTag > aTag`.
+     * to `LastRxFragmentTag > aTag`.
      *
      * @param[in] aTag   A tag value to compare against.
      *
@@ -669,6 +670,16 @@
      */
     uint8_t GetChallengeSize(void) const { return sizeof(mValidPending.mPending.mChallenge); }
 
+#if OPENTHREAD_CONFIG_UPTIME_ENABLE
+    /**
+     * This method returns the connection time (in seconds) of the neighbor (seconds since entering `kStateValid`).
+     *
+     * @returns The connection time (in seconds), zero if device is not currently in `kStateValid`.
+     *
+     */
+    uint32_t GetConnectionTime(void) const;
+#endif
+
 #if OPENTHREAD_CONFIG_TIME_SYNC_ENABLE
     /**
      * This method indicates whether or not time sync feature is enabled.
@@ -840,6 +851,9 @@
     // and this neighbor is the Subject.
     LinkMetrics::Metrics mEnhAckProbingMetrics;
 #endif
+#if OPENTHREAD_CONFIG_UPTIME_ENABLE
+    uint32_t mConnectionStart;
+#endif
 };
 
 #if OPENTHREAD_FTD
@@ -859,7 +873,7 @@
     class AddressIteratorBuilder;
 
 public:
-    static constexpr uint8_t kMaxRequestTlvs = 5;
+    static constexpr uint8_t kMaxRequestTlvs = 6;
 
     /**
      * This class represents diagnostic information for a Thread Child.
@@ -1252,7 +1266,7 @@
     /**
      * This method returns MLR state of an IPv6 multicast address.
      *
-     * @note The @p aAdddress reference MUST be from `IterateIp6Addresses()` or `AddressIterator`.
+     * @note The @p aAddress reference MUST be from `IterateIp6Addresses()` or `AddressIterator`.
      *
      * @param[in] aAddress  The IPv6 multicast address.
      *
@@ -1264,7 +1278,7 @@
     /**
      * This method sets MLR state of an IPv6 multicast address.
      *
-     * @note The @p aAdddress reference MUST be from `IterateIp6Addresses()` or `AddressIterator`.
+     * @note The @p aAddress reference MUST be from `IterateIp6Addresses()` or `AddressIterator`.
      *
      * @param[in] aAddress  The IPv6 multicast address.
      * @param[in] aState    The target MLR state.
diff --git a/src/core/thread/uri_paths.cpp b/src/core/thread/uri_paths.cpp
index 112de36..ff26d2e 100644
--- a/src/core/thread/uri_paths.cpp
+++ b/src/core/thread/uri_paths.cpp
@@ -161,4 +161,44 @@
     return uri;
 }
 
+template <> const char *UriToString<kUriAddressError>(void) { return "AddressError"; }
+template <> const char *UriToString<kUriAddressNotify>(void) { return "AddressNotify"; }
+template <> const char *UriToString<kUriAddressQuery>(void) { return "AddressQuery"; }
+template <> const char *UriToString<kUriAddressRelease>(void) { return "AddressRelease"; }
+template <> const char *UriToString<kUriAddressSolicit>(void) { return "AddressSolicit"; }
+template <> const char *UriToString<kUriServerData>(void) { return "ServerData"; }
+template <> const char *UriToString<kUriAnycastLocate>(void) { return "AnycastLocate"; }
+template <> const char *UriToString<kUriBackboneAnswer>(void) { return "BackboneAnswer"; }
+template <> const char *UriToString<kUriBackboneMlr>(void) { return "BackboneMlr"; }
+template <> const char *UriToString<kUriBackboneQuery>(void) { return "BackboneQuery"; }
+template <> const char *UriToString<kUriAnnounceBegin>(void) { return "AnnounceBegin"; }
+template <> const char *UriToString<kUriActiveGet>(void) { return "ActiveGet"; }
+template <> const char *UriToString<kUriActiveSet>(void) { return "ActiveSet"; }
+template <> const char *UriToString<kUriCommissionerKeepAlive>(void) { return "CommissionerKeepAlive"; }
+template <> const char *UriToString<kUriCommissionerGet>(void) { return "CommissionerGet"; }
+template <> const char *UriToString<kUriCommissionerPetition>(void) { return "CommissionerPetition"; }
+template <> const char *UriToString<kUriCommissionerSet>(void) { return "CommissionerSet"; }
+template <> const char *UriToString<kUriDatasetChanged>(void) { return "DatasetChanged"; }
+template <> const char *UriToString<kUriEnergyReport>(void) { return "EnergyReport"; }
+template <> const char *UriToString<kUriEnergyScan>(void) { return "EnergyScan"; }
+template <> const char *UriToString<kUriJoinerEntrust>(void) { return "JoinerEntrust"; }
+template <> const char *UriToString<kUriJoinerFinalize>(void) { return "JoinerFinalize"; }
+template <> const char *UriToString<kUriLeaderKeepAlive>(void) { return "LeaderKeepAlive"; }
+template <> const char *UriToString<kUriLeaderPetition>(void) { return "LeaderPetition"; }
+template <> const char *UriToString<kUriPanIdConflict>(void) { return "PanIdConflict"; }
+template <> const char *UriToString<kUriPendingGet>(void) { return "PendingGet"; }
+template <> const char *UriToString<kUriPanIdQuery>(void) { return "PanIdQuery"; }
+template <> const char *UriToString<kUriPendingSet>(void) { return "PendingSet"; }
+template <> const char *UriToString<kUriRelayRx>(void) { return "RelayRx"; }
+template <> const char *UriToString<kUriRelayTx>(void) { return "RelayTx"; }
+template <> const char *UriToString<kUriProxyRx>(void) { return "ProxyRx"; }
+template <> const char *UriToString<kUriProxyTx>(void) { return "ProxyTx"; }
+template <> const char *UriToString<kUriDiagnosticGetAnswer>(void) { return "DiagGetAnswer"; }
+template <> const char *UriToString<kUriDiagnosticGetRequest>(void) { return "DiagGetRequest"; }
+template <> const char *UriToString<kUriDiagnosticGetQuery>(void) { return "DiagGetQuery"; }
+template <> const char *UriToString<kUriDiagnosticReset>(void) { return "DiagReset"; }
+template <> const char *UriToString<kUriDuaRegistrationNotify>(void) { return "DuaRegNotify"; }
+template <> const char *UriToString<kUriDuaRegistrationRequest>(void) { return "DuaRegRequest"; }
+template <> const char *UriToString<kUriMlr>(void) { return "Mlr"; }
+
 } // namespace ot
diff --git a/src/core/thread/uri_paths.hpp b/src/core/thread/uri_paths.hpp
index e43928e..01ef7b0 100644
--- a/src/core/thread/uri_paths.hpp
+++ b/src/core/thread/uri_paths.hpp
@@ -108,6 +108,57 @@
  */
 Uri UriFromPath(const char *aPath);
 
+/**
+ * This template function converts a given URI to a human-readable string.
+ *
+ * @tparam kUri   The URI to convert to string.
+ *
+ * @returns The string representation of @p kUri.
+ *
+ */
+template <Uri kUri> const char *UriToString(void);
+
+// Declaring specializations of `UriToString` for every `Uri`
+template <> const char *UriToString<kUriAddressError>(void);
+template <> const char *UriToString<kUriAddressNotify>(void);
+template <> const char *UriToString<kUriAddressQuery>(void);
+template <> const char *UriToString<kUriAddressRelease>(void);
+template <> const char *UriToString<kUriAddressSolicit>(void);
+template <> const char *UriToString<kUriServerData>(void);
+template <> const char *UriToString<kUriAnycastLocate>(void);
+template <> const char *UriToString<kUriBackboneAnswer>(void);
+template <> const char *UriToString<kUriBackboneMlr>(void);
+template <> const char *UriToString<kUriBackboneQuery>(void);
+template <> const char *UriToString<kUriAnnounceBegin>(void);
+template <> const char *UriToString<kUriActiveGet>(void);
+template <> const char *UriToString<kUriActiveSet>(void);
+template <> const char *UriToString<kUriCommissionerKeepAlive>(void);
+template <> const char *UriToString<kUriCommissionerGet>(void);
+template <> const char *UriToString<kUriCommissionerPetition>(void);
+template <> const char *UriToString<kUriCommissionerSet>(void);
+template <> const char *UriToString<kUriDatasetChanged>(void);
+template <> const char *UriToString<kUriEnergyReport>(void);
+template <> const char *UriToString<kUriEnergyScan>(void);
+template <> const char *UriToString<kUriJoinerEntrust>(void);
+template <> const char *UriToString<kUriJoinerFinalize>(void);
+template <> const char *UriToString<kUriLeaderKeepAlive>(void);
+template <> const char *UriToString<kUriLeaderPetition>(void);
+template <> const char *UriToString<kUriPanIdConflict>(void);
+template <> const char *UriToString<kUriPendingGet>(void);
+template <> const char *UriToString<kUriPanIdQuery>(void);
+template <> const char *UriToString<kUriPendingSet>(void);
+template <> const char *UriToString<kUriRelayRx>(void);
+template <> const char *UriToString<kUriRelayTx>(void);
+template <> const char *UriToString<kUriProxyRx>(void);
+template <> const char *UriToString<kUriProxyTx>(void);
+template <> const char *UriToString<kUriDiagnosticGetAnswer>(void);
+template <> const char *UriToString<kUriDiagnosticGetRequest>(void);
+template <> const char *UriToString<kUriDiagnosticGetQuery>(void);
+template <> const char *UriToString<kUriDiagnosticReset>(void);
+template <> const char *UriToString<kUriDuaRegistrationNotify>(void);
+template <> const char *UriToString<kUriDuaRegistrationRequest>(void);
+template <> const char *UriToString<kUriMlr>(void);
+
 } // namespace ot
 
 #endif // URI_PATHS_HPP_
diff --git a/src/core/utils/history_tracker.cpp b/src/core/utils/history_tracker.cpp
index adb9005..2bcda12 100644
--- a/src/core/utils/history_tracker.cpp
+++ b/src/core/utils/history_tracker.cpp
@@ -82,7 +82,7 @@
     return;
 }
 
-void HistoryTracker::RecordMessage(const Message &aMessage, const Mac::Address &aMacAddresss, MessageType aType)
+void HistoryTracker::RecordMessage(const Message &aMessage, const Mac::Address &aMacAddress, MessageType aType)
 {
     MessageInfo *entry = nullptr;
     Ip6::Headers headers;
@@ -125,7 +125,7 @@
     VerifyOrExit(entry != nullptr);
 
     entry->mPayloadLength        = headers.GetIp6Header().GetPayloadLength();
-    entry->mNeighborRloc16       = aMacAddresss.IsShort() ? aMacAddresss.GetShort() : kInvalidRloc16;
+    entry->mNeighborRloc16       = aMacAddress.IsShort() ? aMacAddress.GetShort() : kInvalidRloc16;
     entry->mSource.mAddress      = headers.GetSourceAddress();
     entry->mSource.mPort         = headers.GetSourcePort();
     entry->mDestination.mAddress = headers.GetDestinationAddress();
@@ -138,9 +138,9 @@
     entry->mTxSuccess            = (aType == kTxMessage) ? aMessage.GetTxSuccess() : true;
     entry->mPriority             = aMessage.GetPriority();
 
-    if (aMacAddresss.IsExtended())
+    if (aMacAddress.IsExtended())
     {
-        Neighbor *neighbor = Get<NeighborTable>().FindNeighbor(aMacAddresss, Neighbor::kInStateAnyExceptInvalid);
+        Neighbor *neighbor = Get<NeighborTable>().FindNeighbor(aMacAddress, Neighbor::kInStateAnyExceptInvalid);
 
         if (neighbor != nullptr)
         {
diff --git a/src/core/utils/history_tracker.hpp b/src/core/utils/history_tracker.hpp
index a7bcee2..c361393 100644
--- a/src/core/utils/history_tracker.hpp
+++ b/src/core/utils/history_tracker.hpp
@@ -99,9 +99,9 @@
     static constexpr uint16_t kEntryAgeStringSize = OT_HISTORY_TRACKER_ENTRY_AGE_STRING_SIZE;
 
     /**
-     * This constants specifid no next hop.
+     * This constants specified no next hop.
      *
-     * Used for `mNextHop` in `RouteInfo` struture.
+     * Used for `mNextHop` in `RouteInfo` structure.
      *
      */
     static constexpr uint8_t kNoNextHop = OT_HISTORY_TRACKER_NO_NEXT_HOP;
diff --git a/src/core/utils/parse_cmdline.cpp b/src/core/utils/parse_cmdline.cpp
index ac81464..3f30c83 100644
--- a/src/core/utils/parse_cmdline.cpp
+++ b/src/core/utils/parse_cmdline.cpp
@@ -146,8 +146,8 @@
 
     enum : uint64_t
     {
-        kMaxHexBeforeOveflow = (0xffffffffffffffffULL / 16),
-        kMaxDecBeforeOverlow = (0xffffffffffffffffULL / 10),
+        kMaxHexBeforeOverflow = (0xffffffffffffffffULL / 16),
+        kMaxDecBeforeOverflow = (0xffffffffffffffffULL / 10),
     };
 
     VerifyOrExit(aString != nullptr, error = kErrorInvalidArgs);
@@ -164,7 +164,7 @@
         uint64_t newValue;
 
         SuccessOrExit(error = isHex ? ParseHexDigit(*cur, digit) : ParseDigit(*cur, digit));
-        VerifyOrExit(value <= (isHex ? kMaxHexBeforeOveflow : kMaxDecBeforeOverlow), error = kErrorInvalidArgs);
+        VerifyOrExit(value <= (isHex ? kMaxHexBeforeOverflow : kMaxDecBeforeOverflow), error = kErrorInvalidArgs);
         value    = isHex ? (value << 4) : (value * 10);
         newValue = value + digit;
         VerifyOrExit(newValue >= value, error = kErrorInvalidArgs);
@@ -201,14 +201,14 @@
 {
     Error    error;
     uint64_t value;
-    bool     isNegavtive = false;
+    bool     isNegative = false;
 
     VerifyOrExit(aString != nullptr, error = kErrorInvalidArgs);
 
     if (*aString == '-')
     {
         aString++;
-        isNegavtive = true;
+        isNegative = true;
     }
     else if (*aString == '+')
     {
@@ -216,10 +216,10 @@
     }
 
     SuccessOrExit(error = ParseAsUint64(aString, value));
-    VerifyOrExit(value <= (isNegavtive ? static_cast<uint64_t>(-static_cast<int64_t>(NumericLimits<int32_t>::kMin))
-                                       : static_cast<uint64_t>(NumericLimits<int32_t>::kMax)),
+    VerifyOrExit(value <= (isNegative ? static_cast<uint64_t>(-static_cast<int64_t>(NumericLimits<int32_t>::kMin))
+                                      : static_cast<uint64_t>(NumericLimits<int32_t>::kMax)),
                  error = kErrorInvalidArgs);
-    aInt32 = static_cast<int32_t>(isNegavtive ? -static_cast<int64_t>(value) : static_cast<int64_t>(value));
+    aInt32 = static_cast<int32_t>(isNegative ? -static_cast<int64_t>(value) : static_cast<int64_t>(value));
 
 exit:
     return error;
@@ -250,36 +250,13 @@
 
 Error ParseAsIp6Prefix(const char *aString, otIp6Prefix &aPrefix)
 {
-    enum : uint8_t
-    {
-        kMaxIp6AddressStringSize = 45,
-    };
-
-    Error       error = kErrorInvalidArgs;
-    char        string[kMaxIp6AddressStringSize];
-    const char *prefixLengthStr;
-
-    VerifyOrExit(aString != nullptr);
-
-    prefixLengthStr = StringFind(aString, '/');
-    VerifyOrExit(prefixLengthStr != nullptr);
-
-    VerifyOrExit(prefixLengthStr - aString < static_cast<int32_t>(sizeof(string)));
-
-    memcpy(string, aString, static_cast<uint8_t>(prefixLengthStr - aString));
-    string[prefixLengthStr - aString] = '\0';
-
-    SuccessOrExit(static_cast<Ip6::Address &>(aPrefix.mPrefix).FromString(string));
-    error = ParseAsUint8(prefixLengthStr + 1, aPrefix.mLength);
-
-exit:
-    return error;
+    return (aString != nullptr) ? otIp6PrefixFromString(aString, &aPrefix) : kErrorInvalidArgs;
 }
 #endif // #if OPENTHREAD_FTD || OPENTHREAD_MTD
 
 enum HexStringParseMode
 {
-    kModeExtactSize,   // Parse hex string expecting an exact size (number of bytes when parsed).
+    kModeExactSize,    // Parse hex string expecting an exact size (number of bytes when parsed).
     kModeUpToSize,     // Parse hex string expecting less than or equal a given size.
     kModeAllowPartial, // Allow parsing of partial segments.
 };
@@ -299,7 +276,7 @@
 
     switch (aMode)
     {
-    case kModeExtactSize:
+    case kModeExactSize:
         VerifyOrExit(expectedSize == aSize, error = kErrorInvalidArgs);
         break;
     case kModeUpToSize:
@@ -353,7 +330,7 @@
 
 Error ParseAsHexString(const char *aString, uint8_t *aBuffer, uint16_t aSize)
 {
-    return ParseHexString(aString, aSize, aBuffer, kModeExtactSize);
+    return ParseHexString(aString, aSize, aBuffer, kModeExactSize);
 }
 
 Error ParseAsHexString(const char *aString, uint16_t &aSize, uint8_t *aBuffer)
diff --git a/src/core/utils/parse_cmdline.hpp b/src/core/utils/parse_cmdline.hpp
index 76bb213..777a50a 100644
--- a/src/core/utils/parse_cmdline.hpp
+++ b/src/core/utils/parse_cmdline.hpp
@@ -291,7 +291,7 @@
  * @param[out]    aBuffer    A pointer to a buffer to output the parsed byte sequence.
  *
  * @retval kErrorNone        The string was parsed successfully to the end of string.
- * @retval kErrorPedning     The string segment was parsed successfully, but there are additional bytes remaining
+ * @retval kErrorPending     The string segment was parsed successfully, but there are additional bytes remaining
  *                           to be parsed.
  * @retval kErrorInvalidArgs The string does not contain valid format hex digits.
  *
@@ -673,6 +673,11 @@
 
 template <> inline otError Arg::ParseAs(int32_t &aValue) const { return ParseAsInt32(aValue); }
 
+template <> inline otError Arg::ParseAs(const char *&aValue) const
+{
+    return IsEmpty() ? OT_ERROR_INVALID_ARGS : (aValue = GetCString(), OT_ERROR_NONE);
+}
+
 #if OPENTHREAD_FTD || OPENTHREAD_MTD
 
 template <> inline otError Arg::ParseAs(otIp6Address &aValue) const { return ParseAsIp6Address(aValue); }
diff --git a/src/core/utils/ping_sender.cpp b/src/core/utils/ping_sender.cpp
index 158a754..d1565ef 100644
--- a/src/core/utils/ping_sender.cpp
+++ b/src/core/utils/ping_sender.cpp
@@ -135,7 +135,7 @@
     messageInfo.mHopLimit          = mConfig.mHopLimit;
     messageInfo.mAllowZeroHopLimit = mConfig.mAllowZeroHopLimit;
 
-    message = Get<Ip6::Icmp>().NewMessage(0);
+    message = Get<Ip6::Icmp>().NewMessage();
     VerifyOrExit(message != nullptr);
 
     SuccessOrExit(message->Append(HostSwap32(now.GetValue())));
diff --git a/src/core/utils/ping_sender.hpp b/src/core/utils/ping_sender.hpp
index 81d5288..9eb0a91 100644
--- a/src/core/utils/ping_sender.hpp
+++ b/src/core/utils/ping_sender.hpp
@@ -130,7 +130,7 @@
     private:
         static constexpr uint16_t kDefaultSize     = OPENTHREAD_CONFIG_PING_SENDER_DEFAULT_SIZE;
         static constexpr uint16_t kDefaultCount    = OPENTHREAD_CONFIG_PING_SENDER_DEFAULT_COUNT;
-        static constexpr uint32_t kDefaultInterval = OPENTHREAD_CONFIG_PING_SENDER_DEFAULT_INTEVRAL;
+        static constexpr uint32_t kDefaultInterval = OPENTHREAD_CONFIG_PING_SENDER_DEFAULT_INTERVAL;
         static constexpr uint32_t kDefaultTimeout  = OPENTHREAD_CONFIG_PING_SENDER_DEFAULT_TIMEOUT;
 
         void SetUnspecifiedToDefault(void);
diff --git a/src/core/utils/power_calibration.hpp b/src/core/utils/power_calibration.hpp
index 838a552..e16775b 100644
--- a/src/core/utils/power_calibration.hpp
+++ b/src/core/utils/power_calibration.hpp
@@ -62,7 +62,7 @@
     explicit PowerCalibration(Instance &aInstance);
 
     /**
-     * Add a calibrated power of the specificed channel to the power calibration table.
+     * Add a calibrated power of the specified channel to the power calibration table.
      *
      * @param[in] aChannel                The radio channel.
      * @param[in] aActualPower            The actual power in 0.01dBm.
diff --git a/src/core/utils/slaac_address.hpp b/src/core/utils/slaac_address.hpp
index d9aecce..1784d33 100644
--- a/src/core/utils/slaac_address.hpp
+++ b/src/core/utils/slaac_address.hpp
@@ -132,7 +132,7 @@
      * @param[in]      aNetworkId          A pointer to a byte array of Network_ID to generate IID.
      * @param[in]      aNetworkIdLength    The size of array @p aNetworkId.
      * @param[in,out]  aDadCounter         A pointer to the DAD_Counter that is employed to resolve Duplicate
-     *                                     Address Detection connflicts.
+     *                                     Address Detection conflicts.
      *
      * @retval kErrorNone    If successfully generated the IID.
      * @retval kErrorFailed  If no valid IID was generated.
diff --git a/src/lib/spinel/openthread-spinel-config.h b/src/lib/spinel/openthread-spinel-config.h
index 58ff993..d8dec50 100644
--- a/src/lib/spinel/openthread-spinel-config.h
+++ b/src/lib/spinel/openthread-spinel-config.h
@@ -34,6 +34,8 @@
 #ifndef OPENTHREAD_SPINEL_CONFIG_H_
 #define OPENTHREAD_SPINEL_CONFIG_H_
 
+#include "openthread-core-config.h"
+
 /**
  * @def OPENTHREAD_SPINEL_CONFIG_OPENTHREAD_MESSAGE_ENABLE
  *
diff --git a/src/lib/spinel/radio_spinel.hpp b/src/lib/spinel/radio_spinel.hpp
index a770181..d14f7c0 100644
--- a/src/lib/spinel/radio_spinel.hpp
+++ b/src/lib/spinel/radio_spinel.hpp
@@ -892,7 +892,7 @@
 
 #if OPENTHREAD_CONFIG_PLATFORM_POWER_CALIBRATION_ENABLE
     /**
-     * Add a calibrated power of the specificed channel to the power calibration table.
+     * Add a calibrated power of the specified channel to the power calibration table.
      *
      * @param[in] aChannel                The radio channel.
      * @param[in] aActualPower            The actual power in 0.01dBm.
@@ -962,6 +962,7 @@
 
     static void HandleReceivedFrame(void *aContext);
 
+    void    ResetRcp(bool aResetRadio);
     otError CheckSpinelVersion(void);
     otError CheckRadioCapabilities(void);
     otError CheckRcpApiVersion(bool aSupportsRcpApiVersion, bool aSupportsMinHostRcpApiVersion);
@@ -998,7 +999,7 @@
                                         spinel_prop_key_t aKey,
                                         const char       *aFormat,
                                         va_list           aArgs);
-    otError WaitResponse(void);
+    otError WaitResponse(bool aHandleRcpTimeout = true);
     otError SendCommand(uint32_t          aCommand,
                         spinel_prop_key_t aKey,
                         spinel_tid_t      aTid,
diff --git a/src/lib/spinel/radio_spinel_impl.hpp b/src/lib/spinel/radio_spinel_impl.hpp
index 4bdc033..f520c29 100644
--- a/src/lib/spinel/radio_spinel_impl.hpp
+++ b/src/lib/spinel/radio_spinel_impl.hpp
@@ -230,23 +230,7 @@
     mResetRadioOnStartup = aResetRadio;
 #endif
 
-    if (aResetRadio)
-    {
-        SuccessOrExit(error = SendReset(SPINEL_RESET_STACK));
-        SuccessOrDie(mSpinelInterface.ResetConnection());
-    }
-
-    SuccessOrExit(error = WaitResponse());
-
-#if OPENTHREAD_SPINEL_CONFIG_RCP_RESTORATION_MAX_COUNT > 0
-    while (mRcpFailed)
-    {
-        RecoverFromRcpFailure();
-    }
-#endif
-
-    VerifyOrExit(mIsReady, error = OT_ERROR_FAILED);
-
+    ResetRcp(aResetRadio);
     SuccessOrExit(error = CheckSpinelVersion());
     SuccessOrExit(error = Get(SPINEL_PROP_NCP_VERSION, SPINEL_DATATYPE_UTF8_S, mVersion, sizeof(mVersion)));
     SuccessOrExit(error = Get(SPINEL_PROP_HWADDR, SPINEL_DATATYPE_EUI64_S, mIeeeEui64.m8));
@@ -280,6 +264,43 @@
 }
 
 template <typename InterfaceType, typename ProcessContextType>
+void RadioSpinel<InterfaceType, ProcessContextType>::ResetRcp(bool aResetRadio)
+{
+    bool hardwareReset;
+    bool resetDone = false;
+
+    mIsReady    = false;
+    mWaitingKey = SPINEL_PROP_LAST_STATUS;
+
+    if (aResetRadio && (SendReset(SPINEL_RESET_STACK) == OT_ERROR_NONE) && (WaitResponse(false) == OT_ERROR_NONE))
+    {
+        otLogInfoPlat("Software reset RCP successfully");
+        ExitNow(resetDone = true);
+    }
+
+    hardwareReset = (mSpinelInterface.HardwareReset() == OT_ERROR_NONE);
+    SuccessOrExit(WaitResponse(false));
+
+    resetDone = true;
+
+    if (hardwareReset)
+    {
+        otLogInfoPlat("Hardware reset RCP successfully");
+    }
+    else
+    {
+        otLogInfoPlat("RCP self reset successfully");
+    }
+
+exit:
+    if (!resetDone)
+    {
+        otLogCritPlat("Failed to reset RCP!");
+        DieNow(OT_EXIT_FAILURE);
+    }
+}
+
+template <typename InterfaceType, typename ProcessContextType>
 otError RadioSpinel<InterfaceType, ProcessContextType>::CheckSpinelVersion(void)
 {
     otError      error = OT_ERROR_NONE;
@@ -1699,7 +1720,7 @@
 }
 
 template <typename InterfaceType, typename ProcessContextType>
-otError RadioSpinel<InterfaceType, ProcessContextType>::WaitResponse(void)
+otError RadioSpinel<InterfaceType, ProcessContextType>::WaitResponse(bool aHandleRcpTimeout)
 {
     uint64_t end = otPlatTimeGet() + kMaxWaitTime * US_PER_MS;
 
@@ -1713,7 +1734,10 @@
         if ((end <= now) || (mSpinelInterface.WaitForFrame(end - now) != OT_ERROR_NONE))
         {
             otLogWarnPlat("Wait for response timeout");
-            HandleRcpTimeout();
+            if (aHandleRcpTimeout)
+            {
+                HandleRcpTimeout();
+            }
             ExitNow(mError = OT_ERROR_NONE);
         }
     } while (mWaitingTid || !mIsReady);
@@ -2274,6 +2298,14 @@
 #if OPENTHREAD_SPINEL_CONFIG_RCP_RESTORATION_MAX_COUNT > 0
     mRcpFailed = true;
 #else
+    if (!mIsReady)
+    {
+        otLogCritPlat("Failed to communicate with RCP - no response from RCP during initialization");
+        otLogCritPlat("This is not a bug and typically due a config error (wrong URL parameters) or bad RCP image:");
+        otLogCritPlat("- Make sure RCP is running the correct firmware");
+        otLogCritPlat("- Double check the config parameters passed as `RadioURL` input");
+    }
+
     DieNow(OT_EXIT_RADIO_SPINEL_NO_RESPONSE);
 #endif
 }
@@ -2305,24 +2337,14 @@
 
     mState = kStateDisabled;
     mRxFrameBuffer.Clear();
-    mSpinelInterface.OnRcpReset();
     mCmdTidsInUse = 0;
     mCmdNextTid   = 1;
     mTxRadioTid   = 0;
     mWaitingTid   = 0;
-    mWaitingKey   = SPINEL_PROP_LAST_STATUS;
     mError        = OT_ERROR_NONE;
-    mIsReady      = false;
     mIsTimeSynced = false;
 
-    if (mResetRadioOnStartup)
-    {
-        SuccessOrDie(SendReset(SPINEL_RESET_STACK));
-        SuccessOrDie(mSpinelInterface.ResetConnection());
-    }
-
-    SuccessOrDie(WaitResponse());
-
+    ResetRcp(mResetRadioOnStartup);
     SuccessOrDie(Set(SPINEL_PROP_PHY_ENABLED, SPINEL_DATATYPE_BOOL_S, true));
     mState = kStateSleep;
 
diff --git a/src/lib/spinel/radio_spinel_metrics.h b/src/lib/spinel/radio_spinel_metrics.h
index 438e14c..91d4d76 100644
--- a/src/lib/spinel/radio_spinel_metrics.h
+++ b/src/lib/spinel/radio_spinel_metrics.h
@@ -46,7 +46,7 @@
 typedef struct otRadioSpinelMetrics
 {
     uint32_t mRcpTimeoutCount;         ///< The number of RCP timeouts.
-    uint32_t mRcpUnexpectedResetCount; ///< The number of RCP unexcepted resets.
+    uint32_t mRcpUnexpectedResetCount; ///< The number of RCP unexpected resets.
     uint32_t mRcpRestorationCount;     ///< The number of RCP restorations.
     uint32_t mSpinelParseErrorCount;   ///< The number of spinel frame parse errors.
 } otRadioSpinelMetrics;
diff --git a/src/lib/spinel/spinel.h b/src/lib/spinel/spinel.h
index 0324729..1f4476a 100644
--- a/src/lib/spinel/spinel.h
+++ b/src/lib/spinel/spinel.h
@@ -1625,14 +1625,14 @@
      *
      * For GPIOs configured as inputs:
      *
-     * *   `CMD_PROP_VAUE_GET`: The value of the associated bit describes the
+     * *   `CMD_PROP_VALUE_GET`: The value of the associated bit describes the
      *     logic level read from the pin.
      * *   `CMD_PROP_VALUE_SET`: The value of the associated bit is ignored
      *     for these pins.
      *
      * For GPIOs configured as outputs:
      *
-     * *   `CMD_PROP_VAUE_GET`: The value of the associated bit is
+     * *   `CMD_PROP_VALUE_GET`: The value of the associated bit is
      *     implementation specific.
      * *   `CMD_PROP_VALUE_SET`: The value of the associated bit determines
      *     the new logic level of the output. If this pin is configured as an
@@ -1641,7 +1641,7 @@
      *
      * For GPIOs which are not specified in `PROP_GPIO_CONFIG`:
      *
-     * *   `CMD_PROP_VAUE_GET`: The value of the associated bit is
+     * *   `CMD_PROP_VALUE_GET`: The value of the associated bit is
      *     implementation specific.
      * *   `CMD_PROP_VALUE_SET`: The value of the associated bit MUST be
      *     ignored by the NCP.
@@ -1991,7 +1991,7 @@
      * Channel energy result will be reported by emissions
      * of `PROP_MAC_ENERGY_SCAN_RESULT` (per channel).
      *
-     * Set to `SCAN_STATE_DISOVER` to start a Thread MLE discovery
+     * Set to `SCAN_STATE_DISCOVER` to start a Thread MLE discovery
      * scan operation. Discovery scan result will be emitted from
      * `PROP_MAC_SCAN_BEACON`.
      *
@@ -2472,7 +2472,7 @@
      *  `6`: Route Prefix
      *  `C`: Prefix length in bits
      *  `b`: Stable flag
-     *  `C`: Route flags (SPINEL_ROUTE_FLAG_* and SPINEL_ROUTE_PREFERNCE_* definitions)
+     *  `C`: Route flags (SPINEL_ROUTE_FLAG_* and SPINEL_ROUTE_PREFERENCE_* definitions)
      *  `b`: "Is defined locally" flag. Set if this route info was locally
      *       defined as part of local network data. Assumed to be true for set,
      *       insert and replace. Clear if the route is part of partition's network
@@ -2984,7 +2984,7 @@
     /** Format: `A(t(iD))` - Write only
      *
      * The formatting of this property follows the same rules as in SPINEL_PROP_THREAD_MGMT_SET_ACTIVE_DATASET. This
-     * property further allows the sender to not include a value associated with properties in formating of `t(iD)`,
+     * property further allows the sender to not include a value associated with properties in formatting of `t(iD)`,
      * i.e., it should accept either a `t(iD)` or a `t(i)` encoding (in both cases indicating that the associated
      * Dataset property should be requested as part of MGMT_GET command).
      *
@@ -3266,7 +3266,7 @@
      * Write to this property initiates update of Multicast Listeners Table on the primary BBR.
      * If the write succeeded, the result of network operation will be notified later by the
      * SPINEL_PROP_THREAD_MLR_RESPONSE property. If the write fails, no MLR.req is issued and
-     * notifiaction through the SPINEL_PROP_THREAD_MLR_RESPONSE property will not occur.
+     * notification through the SPINEL_PROP_THREAD_MLR_RESPONSE property will not occur.
      *
      */
     SPINEL_PROP_THREAD_MLR_REQUEST = SPINEL_PROP_THREAD_EXT__BEGIN + 52,
@@ -3319,7 +3319,7 @@
      *
      * The valid values are specified by SPINEL_THREAD_BACKBONE_ROUTER_STATE_<state> enumeration.
      * Backbone functionality will be disabled if SPINEL_THREAD_BACKBONE_ROUTER_STATE_DISABLED
-     * is writted to this property, enabled otherwise.
+     * is written to this property, enabled otherwise.
      *
      */
     SPINEL_PROP_THREAD_BACKBONE_ROUTER_LOCAL_STATE = SPINEL_PROP_THREAD_EXT__BEGIN + 56,
diff --git a/src/lib/spinel/spinel_encoder.hpp b/src/lib/spinel/spinel_encoder.hpp
index 537b4e1..214e813 100644
--- a/src/lib/spinel/spinel_encoder.hpp
+++ b/src/lib/spinel/spinel_encoder.hpp
@@ -123,7 +123,7 @@
     /**
      * This method overwrites the property key with `LAST_STATUS` in a property update command frame.
      *
-     * This method should be only used after a successful `BeginFrame(aHeader, aCommand, aPropertKey)`, otherwise, its
+     * This method should be only used after a successful `BeginFrame(aHeader, aCommand, aPropertyKey)`, otherwise, its
      * behavior is undefined.
      *
      * This method moves the write position back to saved position by `BeginFrame()` and replaces the property key
diff --git a/src/lib/spinel/spinel_interface.hpp b/src/lib/spinel/spinel_interface.hpp
index 585f257..1feafdd 100644
--- a/src/lib/spinel/spinel_interface.hpp
+++ b/src/lib/spinel/spinel_interface.hpp
@@ -36,6 +36,7 @@
 #define POSIX_APP_SPINEL_INTERFACE_HPP_
 
 #include "lib/hdlc/hdlc.hpp"
+#include "lib/spinel/spinel.h"
 
 namespace ot {
 namespace Spinel {
@@ -58,6 +59,23 @@
     typedef Hdlc::MultiFrameBuffer<kMaxFrameSize> RxFrameBuffer;
 
     typedef void (*ReceiveFrameCallback)(void *aContext);
+
+    /**
+     * This method indicates whether or not the frame is the Spinel SPINEL_CMD_RESET frame.
+     *
+     * @param[in] aFrame   A pointer to buffer containing the spinel frame.
+     * @param[in] aLength  The length (number of bytes) in the frame.
+     *
+     * @retval true  If the frame is a Spinel SPINEL_CMD_RESET frame.
+     * @retval false If the frame is not a Spinel SPINEL_CMD_RESET frame.
+     *
+     */
+    static bool IsSpinelResetCommand(const uint8_t *aFrame, uint16_t aLength)
+    {
+        static constexpr uint8_t kSpinelResetCommand[] = {SPINEL_HEADER_FLAG | SPINEL_HEADER_IID_0, SPINEL_CMD_RESET};
+        return (aLength >= sizeof(kSpinelResetCommand)) &&
+               (memcmp(aFrame, kSpinelResetCommand, sizeof(kSpinelResetCommand)) == 0);
+    }
 };
 } // namespace Spinel
 } // namespace ot
diff --git a/src/ncp/ncp_base.cpp b/src/ncp/ncp_base.cpp
index 83b15a2..7807728 100644
--- a/src/ncp/ncp_base.cpp
+++ b/src/ncp/ncp_base.cpp
@@ -285,7 +285,9 @@
 #if OPENTHREAD_CONFIG_MLE_STEERING_DATA_SET_OOB_ENABLE
     memset(&mSteeringDataAddress, 0, sizeof(mSteeringDataAddress));
 #endif
+#if OPENTHREAD_CONFIG_MLE_PARENT_RESPONSE_CALLBACK_API_ENABLE
     otThreadRegisterParentResponseCallback(mInstance, &NcpBase::HandleParentResponseInfo, static_cast<void *>(this));
+#endif
 #endif // OPENTHREAD_FTD
 #if OPENTHREAD_CONFIG_SRP_CLIENT_ENABLE
     otSrpClientSetCallback(mInstance, HandleSrpClientCallback, this);
@@ -671,7 +673,7 @@
 
 #if OPENTHREAD_CONFIG_NCP_ENABLE_PEEK_POKE
 
-void NcpBase::RegisterPeekPokeDelagates(otNcpDelegateAllowPeekPoke aAllowPeekDelegate,
+void NcpBase::RegisterPeekPokeDelegates(otNcpDelegateAllowPeekPoke aAllowPeekDelegate,
                                         otNcpDelegateAllowPeekPoke aAllowPokeDelegate)
 {
     mAllowPeekDelegate = aAllowPeekDelegate;
@@ -2600,14 +2602,13 @@
 // ----------------------------------------------------------------------------
 
 #if OPENTHREAD_CONFIG_NCP_ENABLE_PEEK_POKE
-void otNcpRegisterPeekPokeDelagates(otNcpDelegateAllowPeekPoke aAllowPeekDelegate,
-                                    otNcpDelegateAllowPeekPoke aAllowPokeDelegate)
+void otNcpRegisterPeekPoke(otNcpDelegateAllowPeekPoke aAllowPeekDelegate, otNcpDelegateAllowPeekPoke aAllowPokeDelegate)
 {
     ot::Ncp::NcpBase *ncp = ot::Ncp::NcpBase::GetNcpInstance();
 
     if (ncp != nullptr)
     {
-        ncp->RegisterPeekPokeDelagates(aAllowPeekDelegate, aAllowPokeDelegate);
+        ncp->RegisterPeekPokeDelegates(aAllowPeekDelegate, aAllowPokeDelegate);
     }
 }
 #endif // OPENTHREAD_CONFIG_NCP_ENABLE_PEEK_POKE
diff --git a/src/ncp/ncp_base.hpp b/src/ncp/ncp_base.hpp
index d16693f..995dbc4 100644
--- a/src/ncp/ncp_base.hpp
+++ b/src/ncp/ncp_base.hpp
@@ -126,7 +126,7 @@
      * @param[in] aAllowPokeDelegate      Delegate function pointer for poke operation.
      *
      */
-    void RegisterPeekPokeDelagates(otNcpDelegateAllowPeekPoke aAllowPeekDelegate,
+    void RegisterPeekPokeDelegates(otNcpDelegateAllowPeekPoke aAllowPeekDelegate,
                                    otNcpDelegateAllowPeekPoke aAllowPokeDelegate);
 #endif
 
@@ -272,9 +272,11 @@
     static void HandleNeighborTableChanged(otNeighborTableEvent aEvent, const otNeighborTableEntryInfo *aEntry);
     void        HandleNeighborTableChanged(otNeighborTableEvent aEvent, const otNeighborTableEntryInfo &aEntry);
 
+#if OPENTHREAD_CONFIG_MLE_PARENT_RESPONSE_CALLBACK_API_ENABLE
     static void HandleParentResponseInfo(otThreadParentResponseInfo *aInfo, void *aContext);
     void        HandleParentResponseInfo(const otThreadParentResponseInfo &aInfo);
 #endif
+#endif
 
     static void HandleDatagramFromStack(otMessage *aMessage, void *aContext);
     void        HandleDatagramFromStack(otMessage *aMessage);
diff --git a/src/ncp/ncp_base_ftd.cpp b/src/ncp/ncp_base_ftd.cpp
index ffd4255..fb763c4 100644
--- a/src/ncp/ncp_base_ftd.cpp
+++ b/src/ncp/ncp_base_ftd.cpp
@@ -85,6 +85,7 @@
 // MARK: Property/Status Changed
 // ----------------------------------------------------------------------------
 
+#if OPENTHREAD_CONFIG_MLE_PARENT_RESPONSE_CALLBACK_API_ENABLE
 void NcpBase::HandleParentResponseInfo(otThreadParentResponseInfo *aInfo, void *aContext)
 {
     VerifyOrExit(aInfo && aContext);
@@ -116,6 +117,7 @@
 exit:
     return;
 }
+#endif
 
 void NcpBase::HandleNeighborTableChanged(otNeighborTableEvent aEvent, const otNeighborTableEntryInfo *aEntry)
 {
diff --git a/src/ncp/ncp_base_mtd.cpp b/src/ncp/ncp_base_mtd.cpp
index 1220e15..78141db 100644
--- a/src/ncp/ncp_base_mtd.cpp
+++ b/src/ncp/ncp_base_mtd.cpp
@@ -1047,7 +1047,7 @@
 
     error = otBorderRouterRemoveOnMeshPrefix(mInstance, &ip6Prefix);
 
-    // If prefix was not on the list, "remove" command can be considred
+    // If prefix was not on the list, "remove" command can be considered
     // successful.
 
     if (error == OT_ERROR_NOT_FOUND)
@@ -3659,7 +3659,7 @@
     return error;
 }
 
-static spinel_srp_client_item_state_t SrpClientItemStatetoSpinel(otSrpClientItemState aItemState)
+static spinel_srp_client_item_state_t SrpClientItemStateToSpinel(otSrpClientItemState aItemState)
 {
     spinel_srp_client_item_state_t state = SPINEL_SRP_CLIENT_ITEM_STATE_REMOVED;
 
@@ -3699,7 +3699,7 @@
     otError error;
 
     SuccessOrExit(error = mEncoder.WriteUtf8(aHostInfo.mName != nullptr ? aHostInfo.mName : ""));
-    SuccessOrExit(error = mEncoder.WriteUint8(SrpClientItemStatetoSpinel(aHostInfo.mState)));
+    SuccessOrExit(error = mEncoder.WriteUint8(SrpClientItemStateToSpinel(aHostInfo.mState)));
 
     SuccessOrExit(error = mEncoder.OpenStruct());
 
diff --git a/src/posix/Makefile-posix b/src/posix/Makefile-posix
index 5ddc67b..7fe9722 100644
--- a/src/posix/Makefile-posix
+++ b/src/posix/Makefile-posix
@@ -61,9 +61,9 @@
 LOG_OUTPUT                           ?= PLATFORM_DEFINED
 MAC_FILTER                           ?= 1
 MAX_POWER_TABLE                      ?= 1
-MTD_NETDIAG                          ?= 1
 NEIGHBOR_DISCOVERY_AGENT             ?= 1
 NETDATA_PUBLISHER                    ?= 1
+NETDIAG_CLIENT                       ?= 1
 PING_SENDER                          ?= 1
 READLINE                             ?= readline
 REFERENCE_DEVICE                     ?= 1
diff --git a/src/posix/README.md b/src/posix/README.md
index d45a4a3..ccffb36 100644
--- a/src/posix/README.md
+++ b/src/posix/README.md
@@ -28,7 +28,7 @@
 
 ### Simulation
 
-OpenThread provides an implemenation on the simulation platform which enables running a simulated transceiver on the host.
+OpenThread provides an implementation on the simulation platform which enables running a simulated transceiver on the host.
 
 #### Build
 
diff --git a/src/posix/cli.cmake b/src/posix/cli.cmake
index 4354374..4b7347b 100644
--- a/src/posix/cli.cmake
+++ b/src/posix/cli.cmake
@@ -43,7 +43,7 @@
     ${OT_CFLAGS}
 )
 
-target_link_libraries(ot-cli
+target_link_libraries(ot-cli PRIVATE
     openthread-cli-ftd
     openthread-posix
     openthread-ftd
@@ -57,6 +57,13 @@
     ot-config
 )
 
+if(OT_LINKER_MAP)
+    if("${CMAKE_CXX_COMPILER_ID}" MATCHES "AppleClang")
+        target_link_libraries(ot-cli PRIVATE -Wl,-map,ot-cli.map)
+    else()
+        target_link_libraries(ot-cli PRIVATE -Wl,-Map=ot-cli.map)
+    endif()
+endif()
 
 install(TARGETS ot-cli DESTINATION bin)
 
diff --git a/src/posix/daemon.cmake b/src/posix/daemon.cmake
index cf2c7b7..dd726df 100644
--- a/src/posix/daemon.cmake
+++ b/src/posix/daemon.cmake
@@ -49,6 +49,14 @@
     ot-config
 )
 
+if(OT_LINKER_MAP)
+    if("${CMAKE_CXX_COMPILER_ID}" MATCHES "AppleClang")
+        target_link_libraries(ot-daemon PRIVATE -Wl,-map,ot-daemon.map)
+    else()
+        target_link_libraries(ot-daemon PRIVATE -Wl,-Map=ot-daemon.map)
+    endif()
+endif()
+
 add_executable(ot-ctl
     client.cpp
 )
@@ -68,6 +76,14 @@
     ot-config
 )
 
+if(OT_LINKER_MAP)
+    if("${CMAKE_CXX_COMPILER_ID}" MATCHES "AppleClang")
+        target_link_libraries(ot-ctl PRIVATE -Wl,-map,ot-ctl.map)
+    else()
+        target_link_libraries(ot-ctl PRIVATE -Wl,-Map=ot-ctl.map)
+    endif()
+endif()
+
 target_include_directories(ot-ctl PRIVATE ${COMMON_INCLUDES})
 
 install(TARGETS ot-daemon
diff --git a/src/posix/main.c b/src/posix/main.c
index 9b81e2d..8800ac0 100644
--- a/src/posix/main.c
+++ b/src/posix/main.c
@@ -380,7 +380,7 @@
 #if !OPENTHREAD_POSIX_CONFIG_DAEMON_ENABLE
     otAppCliInit(instance);
 #endif
-    otCliSetUserCommands(kCommands, OT_ARRAY_LENGTH(kCommands), instance);
+    IgnoreError(otCliSetUserCommands(kCommands, OT_ARRAY_LENGTH(kCommands), instance));
 
     while (true)
     {
diff --git a/src/posix/platform/CMakeLists.txt b/src/posix/platform/CMakeLists.txt
index 0a44c88..02a30dc 100644
--- a/src/posix/platform/CMakeLists.txt
+++ b/src/posix/platform/CMakeLists.txt
@@ -95,11 +95,12 @@
     set(OT_CONFIG "openthread-core-posix-config.h" PARENT_SCOPE)
 endif()
 
-set(OT_POSIX_CONFIG_RCP_VENDOR_INTERFACE "vendor_interface_example.cpp"
-       CACHE STRING "vendor interface implementation")
-
 set(CMAKE_EXE_LINKER_FLAGS "-rdynamic ${CMAKE_EXE_LINKER_FLAGS}" PARENT_SCOPE)
 
+if(OT_ANDROID_NDK)
+    target_compile_options(ot-posix-config INTERFACE -Wno-sign-compare)
+endif()
+
 add_library(openthread-posix
     alarm.cpp
     backbone.cpp
@@ -128,9 +129,10 @@
     udp.cpp
     utils.cpp
     virtual_time.cpp
-    ${OT_POSIX_CONFIG_RCP_VENDOR_INTERFACE}
 )
 
+include(vendor.cmake)
+
 target_link_libraries(openthread-posix
     PUBLIC
         openthread-platform
@@ -143,7 +145,7 @@
         ot-config-ftd
         ot-config
         ot-posix-config
-        util
+        $<$<NOT:$<BOOL:${OT_ANDROID_NDK}>>:util>
         $<$<STREQUAL:${CMAKE_SYSTEM_NAME},Linux>:rt>
 )
 
diff --git a/src/posix/platform/FindExampleVendorDeps.cmake b/src/posix/platform/FindExampleVendorDeps.cmake
new file mode 100644
index 0000000..fe45a13
--- /dev/null
+++ b/src/posix/platform/FindExampleVendorDeps.cmake
@@ -0,0 +1,127 @@
+#
+#  Copyright (c) 2023, The OpenThread Authors.
+#  All rights reserved.
+#
+#  Redistribution and use in source and binary forms, with or without
+#  modification, are permitted provided that the following conditions are met:
+#  1. Redistributions of source code must retain the above copyright
+#     notice, this list of conditions and the following disclaimer.
+#  2. Redistributions in binary form must reproduce the above copyright
+#     notice, this list of conditions and the following disclaimer in the
+#     documentation and/or other materials provided with the distribution.
+#  3. Neither the name of the copyright holder nor the
+#     names of its contributors may be used to endorse or promote products
+#     derived from this software without specific prior written permission.
+#
+#  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+#  AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+#  IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+#  ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+#  LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+#  CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+#  SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+#  INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+#  CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+#  ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+#  POSSIBILITY OF SUCH DAMAGE.
+#
+
+#[=======================================================================[.rst:
+FindExampleRcpVendorDeps
+------------------------
+
+This file provides a reference for how to implement an RCP vendor
+dependency CMake module to resolve external libraries and header
+files used by a vendor implementation in the posix library.
+
+The name of this file and the name of the targets it defines are
+conventionally related. For the purpose of this reference, targets
+will be based off of the identifier "ExampleRcpVendorDeps". Derived
+references should be based off of the value of the cache variable,
+"OT_POSIX_CONFIG_RCP_VENDOR_DEPS_PACKAGE".
+
+For more information about package resolution using CMake find modules,
+reference the cmake-developer documentation.
+
+Imported Targets
+^^^^^^^^^^^^^^^^
+
+This module provides the following imported targets, if found:
+
+``ExampleRcpVendorDeps::ExampleRcpVendorDeps``
+  RCP vendor interface library dependencies
+
+Result Variables
+^^^^^^^^^^^^^^^^
+
+This will define the following variables:
+
+``ExampleRcpVendorDeps_FOUND``
+  True if the system has all of the required external dependencies
+``ExampleRcpVendorDeps_INCLUDE_DIRS``
+  Include directories needed by vendor interface
+``ExampleRcpVendorDeps_LIBRARIES``
+  Libraries needed by vendor interface
+
+Cache Variables
+^^^^^^^^^^^^^^^
+
+Vendors modules may configure various cache variables
+while resolving dependencies:
+
+``Dependency0_INCLUDE_DIR``
+  The directory containing include files for dependency 0
+``Dependency0_LIBRARY``
+  The path to the library containing symbols for dependency 0
+``Dependency1_INCLUDE_DIR``
+  The directory containing include files for dependency 1
+``Dependency1_LIBRARY``
+  The path to the library containing symbols for dependency 1
+
+#]=======================================================================]
+
+include(FindPackageHandleStandardArgs)
+
+find_path(Dependency0_INCLUDE_DIR
+    NAMES example0/example.h
+    PATH  ${EXAMPLES_ROOT}/include
+)
+
+find_library(Dependency0_LIBRARY
+    NAMES example0
+    PATH  ${EXAMPLES_ROOT}/lib
+)
+
+find_path(Dependency1_INCLUDE_DIR
+    NAMES example1/example.h
+    PATH ${EXAMPLES_ROOT}/include
+)
+
+find_library(Dependency1_LIBRARY
+    NAMES example1
+    PATH ${EXAMPLES_ROOT}/lib
+)
+
+find_package_handle_standard_args(ExampleRcpVendorDeps
+    FOUND_VAR ExampleRcpVendorDeps_FOUND
+    REQUIRED_VARS Dependency0_INCLUDE_DIR Dependency0_LIBRARY Dependency1_INCLUDE_DIR Dependency1_LIBRARY
+)
+
+if(ExampleRcpVendorDeps_FOUND AND NOT ExampleRcpVendorDeps::ExampleRcpVendorDeps)
+    set(ExampleRcpVendorDeps_INCLUDE_DIRS ${Dependency0_INCLUDE_DIR} ${Dependency1_INCLUDE_DIR})
+    set(ExampleRcpVendorDeps_LIBRARIES ${Dependency0_LIBRARY} ${Dependency1_LIBRARY})
+
+    add_library(ExampleRcpVendorDeps::ExampleRcpVendorDeps UNKNOWN IMPORTED)
+    set_target_properties(ExampleRcpVendorDeps::ExampleRcpVendorDeps PROPERTIES
+        IMPORTED_LOCATION "${ExampleRcpVendorDeps_LIBRARIES}"
+        INTERFACE_INCLUDE_DIRECTORIES "${ExampleRcpVendorDeps_INCLUDE_DIRS}"
+    )
+
+    mark_as_advanced(
+        Dependency0_INCLUDE_DIR
+        Dependency0_LIBRARY
+        Dependency1_INCLUDE_DIR
+        Dependency1_LIBRARY
+    )
+endif()
+
diff --git a/src/posix/platform/backtrace.cpp b/src/posix/platform/backtrace.cpp
index 873ccb3..1f697ca 100644
--- a/src/posix/platform/backtrace.cpp
+++ b/src/posix/platform/backtrace.cpp
@@ -132,6 +132,17 @@
     return;
 }
 #endif // OPENTHREAD_POSIX_CONFIG_ANDROID_ENABLE
+static constexpr uint8_t kNumSignals           = 6;
+static constexpr int     kSignals[kNumSignals] = {SIGABRT, SIGILL, SIGSEGV, SIGBUS, SIGTRAP, SIGFPE};
+static struct sigaction  sSigActions[kNumSignals];
+
+static void resetSignalActions(void)
+{
+    for (uint8_t i = 0; i < kNumSignals; i++)
+    {
+        sigaction(kSignals[i], &sSigActions[i], (struct sigaction *)nullptr);
+    }
+}
 
 static void signalCritical(int sig, siginfo_t *info, void *ucontext)
 {
@@ -144,7 +155,9 @@
     dumpStack();
 
     otLogCritPlat("------------------ END OF CRASH ------------------");
-    exit(EXIT_FAILURE);
+
+    resetSignalActions();
+    raise(sig);
 }
 
 void platformBacktraceInit(void)
@@ -154,12 +167,10 @@
     sigact.sa_sigaction = &signalCritical;
     sigact.sa_flags     = SA_RESTART | SA_SIGINFO | SA_NOCLDWAIT;
 
-    sigaction(SIGABRT, &sigact, (struct sigaction *)nullptr);
-    sigaction(SIGILL, &sigact, (struct sigaction *)nullptr);
-    sigaction(SIGSEGV, &sigact, (struct sigaction *)nullptr);
-    sigaction(SIGBUS, &sigact, (struct sigaction *)nullptr);
-    sigaction(SIGTRAP, &sigact, (struct sigaction *)nullptr);
-    sigaction(SIGFPE, &sigact, (struct sigaction *)nullptr);
+    for (uint8_t i = 0; i < kNumSignals; i++)
+    {
+        sigaction(kSignals[i], &sigact, &sSigActions[i]);
+    }
 }
 #else  // OPENTHREAD_POSIX_CONFIG_ANDROID_ENABLE || defined(__GLIBC__)
 void platformBacktraceInit(void) {}
diff --git a/src/posix/platform/daemon.cpp b/src/posix/platform/daemon.cpp
index acf897e..731b4f2 100644
--- a/src/posix/platform/daemon.cpp
+++ b/src/posix/platform/daemon.cpp
@@ -341,7 +341,6 @@
         if (rval > 0)
         {
             buffer[rval] = '\0';
-            otLogInfoPlat("> %s", reinterpret_cast<const char *>(buffer));
             otCliInputLine(reinterpret_cast<char *>(buffer));
         }
         else
diff --git a/src/posix/platform/hdlc_interface.cpp b/src/posix/platform/hdlc_interface.cpp
index 9315bab..a37427e 100644
--- a/src/posix/platform/hdlc_interface.cpp
+++ b/src/posix/platform/hdlc_interface.cpp
@@ -60,6 +60,7 @@
 #include <openthread/logging.h>
 
 #include "common/code_utils.hpp"
+#include "lib/spinel/spinel.h"
 
 #ifdef __APPLE__
 
@@ -139,8 +140,6 @@
     mInterfaceMetrics.mRcpInterfaceType = OT_POSIX_RCP_BUS_UART;
 }
 
-void HdlcInterface::OnRcpReset(void) { mHdlcDecoder.Reset(); }
-
 otError HdlcInterface::Init(const Url::Url &aRadioUrl)
 {
     otError     error = OT_ERROR_NONE;
@@ -210,6 +209,12 @@
     error = Write(encoderBuffer.GetFrame(), encoderBuffer.GetLength());
 
 exit:
+    if ((error == OT_ERROR_NONE) && ot::Spinel::SpinelInterface::IsSpinelResetCommand(aFrame, aLength))
+    {
+        mHdlcDecoder.Reset();
+        error = ResetConnection();
+    }
+
     return error;
 }
 
diff --git a/src/posix/platform/hdlc_interface.hpp b/src/posix/platform/hdlc_interface.hpp
index f2d5801..dc9fbf8 100644
--- a/src/posix/platform/hdlc_interface.hpp
+++ b/src/posix/platform/hdlc_interface.hpp
@@ -158,16 +158,13 @@
     uint32_t GetBusSpeed(void) const { return mBaudRate; }
 
     /**
-     * This method is called when RCP failure detected and resets internal states of the interface.
+     * This method hardware resets the RCP.
+     *
+     * @retval OT_ERROR_NONE            Successfully reset the RCP.
+     * @retval OT_ERROR_NOT_IMPLEMENT   The hardware reset is not implemented.
      *
      */
-    void OnRcpReset(void);
-
-    /**
-     * This method is called when RCP is reset to recreate the connection with it.
-     *
-     */
-    otError ResetConnection(void);
+    otError HardwareReset(void) { return OT_ERROR_NOT_IMPLEMENTED; }
 
     /**
      * This method returns the RCP interface metrics.
@@ -179,6 +176,12 @@
 
 private:
     /**
+     * This method is called when RCP is reset to recreate the connection with it.
+     *
+     */
+    otError ResetConnection(void);
+
+    /**
      * This method instructs `HdlcInterface` to read and decode data from radio over the socket.
      *
      * If a full HDLC frame is decoded while reading data, this method invokes the `HandleReceivedFrame()` (on the
diff --git a/src/posix/platform/include/openthread/openthread-system.h b/src/posix/platform/include/openthread/openthread-system.h
index 35ae715..bca1a71 100644
--- a/src/posix/platform/include/openthread/openthread-system.h
+++ b/src/posix/platform/include/openthread/openthread-system.h
@@ -60,7 +60,7 @@
     OT_PLATFORM_CONFIG_SPI_DEFAULT_MODE           = 0,       ///< Default SPI Mode: CPOL=0, CPHA=0.
     OT_PLATFORM_CONFIG_SPI_DEFAULT_SPEED_HZ       = 1000000, ///< Default SPI speed in hertz.
     OT_PLATFORM_CONFIG_SPI_DEFAULT_CS_DELAY_US    = 20,      ///< Default delay after SPI C̅S̅ assertion, in µsec.
-    OT_PLATFORM_CONFIG_SPI_DEFAULT_RESET_DELAY_MS = 0, ///< Default delay after R̅E̅S̅E̅T̅ assertion, in miliseconds.
+    OT_PLATFORM_CONFIG_SPI_DEFAULT_RESET_DELAY_MS = 0, ///< Default delay after R̅E̅S̅E̅T̅ assertion, in milliseconds.
     OT_PLATFORM_CONFIG_SPI_DEFAULT_ALIGN_ALLOWANCE =
         16, ///< Default maximum number of 0xFF bytes to clip from start of MISO frame.
     OT_PLATFORM_CONFIG_SPI_DEFAULT_SMALL_PACKET_SIZE =
diff --git a/src/posix/platform/mainloop.hpp b/src/posix/platform/mainloop.hpp
index 4f0fa7c..fd2989b 100644
--- a/src/posix/platform/mainloop.hpp
+++ b/src/posix/platform/mainloop.hpp
@@ -66,7 +66,7 @@
     virtual void Process(const otSysMainloopContext &aContext) = 0;
 
     /**
-     * This method marks desturctor virtual method.
+     * This method marks destructor virtual method.
      *
      */
     virtual ~Source() = default;
@@ -117,7 +117,7 @@
     /**
      * This function returns the Mainloop singleton.
      *
-     * @returns A refernce to the Mainloop singleton.
+     * @returns A reference to the Mainloop singleton.
      *
      */
     static Manager &Get(void);
diff --git a/src/posix/platform/misc.cpp b/src/posix/platform/misc.cpp
index 77ef07a..7e2a82e 100644
--- a/src/posix/platform/misc.cpp
+++ b/src/posix/platform/misc.cpp
@@ -84,7 +84,7 @@
 #else
     otLogCritPlat("assert failed at %s:%d", aFilename, aLineNumber);
 #endif
-    // For debug build, use assert to genreate a core dump
+    // For debug build, use assert to generate a core dump
     assert(false);
     exit(1);
 }
diff --git a/src/posix/platform/multicast_routing.hpp b/src/posix/platform/multicast_routing.hpp
index c55844c..dc8b72a 100644
--- a/src/posix/platform/multicast_routing.hpp
+++ b/src/posix/platform/multicast_routing.hpp
@@ -69,7 +69,7 @@
     {
         kMulticastForwardingCacheExpireTimeout    = 300, //< Expire timeout of Multicast Forwarding Cache (in seconds)
         kMulticastForwardingCacheExpiringInterval = 60,  //< Expire interval of Multicast Forwarding Cache (in seconds)
-        kMulitcastForwardingCacheTableSize =
+        kMulticastForwardingCacheTableSize =
             OPENTHREAD_POSIX_CONFIG_MAX_MULTICAST_FORWARDING_CACHE_TABLE, //< The max size of MFC table.
     };
 
@@ -132,7 +132,7 @@
     void               HandleBackboneMulticastListenerEvent(otBackboneRouterMulticastListenerEvent aEvent,
                                                             const Ip6::Address                    &aAddress);
 
-    MulticastForwardingCache mMulticastForwardingCacheTable[kMulitcastForwardingCacheTableSize];
+    MulticastForwardingCache mMulticastForwardingCacheTable[kMulticastForwardingCacheTableSize];
     uint64_t                 mLastExpireTime;
     int                      mMulticastRouterSock;
 };
diff --git a/src/posix/platform/power_updater.hpp b/src/posix/platform/power_updater.hpp
index 5a8e20a..fb12c23 100644
--- a/src/posix/platform/power_updater.hpp
+++ b/src/posix/platform/power_updater.hpp
@@ -48,7 +48,7 @@
 namespace Posix {
 
 /**
- * This class updates the target power table and calibrated powe table to the RCP.
+ * This class updates the target power table and calibrated power table to the RCP.
  *
  */
 class PowerUpdater
diff --git a/src/posix/platform/radio.cpp b/src/posix/platform/radio.cpp
index 0134e28..00e2c1c 100644
--- a/src/posix/platform/radio.cpp
+++ b/src/posix/platform/radio.cpp
@@ -201,6 +201,8 @@
 #endif // OPENTHREAD_CONFIG_PLATFORM_RADIO_COEX_ENABLE
 }
 
+void *Radio::GetSpinelInstance(void) { return &sRadioSpinel; }
+
 } // namespace Posix
 } // namespace ot
 
@@ -735,6 +737,16 @@
     return error;
 }
 
+otError otPlatDiagRadioTransmitStream(otInstance *aInstance, bool aEnable)
+{
+    OT_UNUSED_VARIABLE(aInstance);
+
+    char cmd[OPENTHREAD_CONFIG_DIAG_CMD_LINE_BUFFER_SIZE];
+
+    snprintf(cmd, sizeof(cmd), "stream %s", aEnable ? "start" : "stop");
+    return sRadioSpinel.PlatDiagProcess(cmd, nullptr, 0);
+}
+
 void otPlatDiagRadioReceived(otInstance *aInstance, otRadioFrame *aFrame, otError aError)
 {
     OT_UNUSED_VARIABLE(aInstance);
diff --git a/src/posix/platform/radio.hpp b/src/posix/platform/radio.hpp
index 1312cb4..6a3d549 100644
--- a/src/posix/platform/radio.hpp
+++ b/src/posix/platform/radio.hpp
@@ -55,6 +55,14 @@
      */
     void Init(void);
 
+    /**
+     * This method acts as an accessor to the spinel instance used by the radio.
+     *
+     * @returns A pointer to the radio's spinel interface instance.
+     *
+     */
+    static void *GetSpinelInstance(void);
+
 private:
     RadioUrl mRadioUrl;
 };
diff --git a/src/posix/platform/radio_url.cpp b/src/posix/platform/radio_url.cpp
index c126663..e062ec9 100644
--- a/src/posix/platform/radio_url.cpp
+++ b/src/posix/platform/radio_url.cpp
@@ -63,8 +63,7 @@
     "    spi-small-packet=[n]          Specify the smallest packet we can receive in a single transaction.\n"  \
     "                                  (larger packets will require two transactions). Default value is 32.\n"
 
-#else
-
+#elif OPENTHREAD_POSIX_CONFIG_RCP_BUS == OT_POSIX_RCP_BUS_UART
 #define OT_RADIO_URL_HELP_BUS                                                                        \
     "    forkpty-arg[=argument string]  Command line arguments for subprocess, can be repeated.\n"   \
     "    spinel+hdlc+uart://${PATH_TO_UART_DEVICE}?${Parameters} for real uart device\n"             \
@@ -76,6 +75,14 @@
     "    uart-flow-control              Enable flow control, disabled by default.\n"                 \
     "    uart-reset                     Reset connection after hard resetting RCP(USB CDC ACM).\n"
 
+#elif OPENTHREAD_POSIX_CONFIG_RCP_BUS == OT_POSIX_RCP_BUS_VENDOR
+
+#ifndef OT_VENDOR_RADIO_URL_HELP_BUS
+#define OT_VENDOR_RADIO_URL_HELP_BUS "\n"
+#endif // OT_VENDOR_RADIO_URL_HELP_BUS
+
+#define OT_RADIO_URL_HELP_BUS OT_VENDOR_RADIO_URL_HELP_BUS
+
 #endif // OPENTHREAD_POSIX_CONFIG_RCP_BUS == OT_POSIX_RCP_BUS_SPI
 
 #if OPENTHREAD_POSIX_CONFIG_MAX_POWER_TABLE_ENABLE
diff --git a/src/posix/platform/resolver.cpp b/src/posix/platform/resolver.cpp
index cc7b0b9..85eda40 100644
--- a/src/posix/platform/resolver.cpp
+++ b/src/posix/platform/resolver.cpp
@@ -34,6 +34,7 @@
 #include <openthread/message.h>
 #include <openthread/udp.h>
 #include <openthread/platform/dns.h>
+#include <openthread/platform/time.h>
 
 #include "common/code_utils.hpp"
 
@@ -51,8 +52,8 @@
 #if OPENTHREAD_CONFIG_DNS_UPSTREAM_QUERY_ENABLE
 
 namespace {
-constexpr char kResolveConfLocation[] = "/etc/resolv.conf";
-constexpr char kNameserverItem[]      = "nameserver";
+constexpr char kResolvConfFullPath[] = "/etc/resolv.conf";
+constexpr char kNameserverItem[]     = "nameserver";
 } // namespace
 
 extern ot::Posix::Resolver gResolver;
@@ -63,25 +64,30 @@
 void Resolver::Init(void)
 {
     memset(mUpstreamTransaction, 0, sizeof(mUpstreamTransaction));
-
     LoadDnsServerListFromConf();
 }
 
+void Resolver::TryRefreshDnsServerList(void)
+{
+    uint64_t now = otPlatTimeGet();
+
+    if (now > mUpstreamDnsServerListFreshness + kDnsServerListCacheTimeoutMs ||
+        (mUpstreamDnsServerCount == 0 && now > mUpstreamDnsServerListFreshness + kDnsServerListNullCacheTimeoutMs))
+    {
+        LoadDnsServerListFromConf();
+    }
+}
+
 void Resolver::LoadDnsServerListFromConf(void)
 {
     std::string   line;
     std::ifstream fp;
 
-    fp.open(kResolveConfLocation);
-    if (fp.bad())
-    {
-        otLogCritPlat("Cannot read %s for domain name servers, default to 127.0.0.1", kResolveConfLocation);
-        mUpstreamDnsServerCount = 1;
-        assert(inet_pton(AF_INET, "127.0.0.1", &mUpstreamDnsServerList[0]) == 1);
-        ExitNow();
-    }
+    mUpstreamDnsServerCount = 0;
 
-    while (std::getline(fp, line) && mUpstreamDnsServerCount < kMaxUpstreamServerCount)
+    fp.open(kResolvConfFullPath);
+
+    while (fp.good() && std::getline(fp, line) && mUpstreamDnsServerCount < kMaxUpstreamServerCount)
     {
         if (line.find(kNameserverItem, 0) == 0)
         {
@@ -89,7 +95,7 @@
 
             if (inet_pton(AF_INET, &line.c_str()[sizeof(kNameserverItem)], &addr) == 1)
             {
-                otLogCritPlat("Got nameserver #%d: %s", mUpstreamDnsServerCount,
+                otLogInfoPlat("Got nameserver #%d: %s", mUpstreamDnsServerCount,
                               &line.c_str()[sizeof(kNameserverItem)]);
                 mUpstreamDnsServerList[mUpstreamDnsServerCount] = addr;
                 mUpstreamDnsServerCount++;
@@ -97,8 +103,12 @@
         }
     }
 
-exit:
-    return;
+    if (mUpstreamDnsServerCount == 0)
+    {
+        otLogCritPlat("No domain name servers found in %s, default to 127.0.0.1", kResolvConfFullPath);
+    }
+
+    mUpstreamDnsServerListFreshness = otPlatTimeGet();
 }
 
 void Resolver::Query(otPlatDnsUpstreamQuery *aTxn, const otMessage *aQuery)
@@ -108,7 +118,7 @@
     uint16_t    length = otMessageGetLength(aQuery);
     sockaddr_in serverAddr;
 
-    Transaction *txn;
+    Transaction *txn = nullptr;
 
     VerifyOrExit(length <= kMaxDnsMessageSize, error = OT_ERROR_NO_BUFS);
     VerifyOrExit(otMessageRead(aQuery, 0, &packet, sizeof(packet)) == length, error = OT_ERROR_NO_BUFS);
@@ -116,6 +126,8 @@
     txn = AllocateTransaction(aTxn);
     VerifyOrExit(txn != nullptr, error = OT_ERROR_NO_BUFS);
 
+    TryRefreshDnsServerList();
+
     serverAddr.sin_family = AF_INET;
     serverAddr.sin_port   = htons(53);
     for (int i = 0; i < mUpstreamDnsServerCount; i++)
@@ -176,12 +188,14 @@
 {
     char       response[kMaxDnsMessageSize];
     ssize_t    readSize;
-    otMessage *message;
+    otError    error   = OT_ERROR_NONE;
+    otMessage *message = nullptr;
 
     VerifyOrExit((readSize = read(aTxn->mUdpFd, response, sizeof(response))) > 0);
 
     message = otUdpNewMessage(gInstance, nullptr);
-    SuccessOrExit(otMessageAppend(message, response, readSize));
+    VerifyOrExit(message != nullptr, error = OT_ERROR_NO_BUFS);
+    SuccessOrExit(error = otMessageAppend(message, response, readSize));
 
     otPlatDnsUpstreamQueryDone(gInstance, aTxn->mThreadTxn, message);
     message = nullptr;
@@ -191,6 +205,10 @@
     {
         otLogInfoPlat("Failed to read response from upstream resolver socket: %d", errno);
     }
+    if (error != OT_ERROR_NONE)
+    {
+        otLogInfoPlat("Failed to forward upstream DNS response: %s", otThreadErrorToString(error));
+    }
     if (message != nullptr)
     {
         otMessageFree(message);
diff --git a/src/posix/platform/resolver.hpp b/src/posix/platform/resolver.hpp
index fe967d7..ea7da3b 100644
--- a/src/posix/platform/resolver.hpp
+++ b/src/posix/platform/resolver.hpp
@@ -90,6 +90,9 @@
     void Process(const fd_set *aReadFdSet, const fd_set *aErrorFdSet);
 
 private:
+    static constexpr uint64_t kDnsServerListNullCacheTimeoutMs = 1 * 60 * 1000;  // 1 minute
+    static constexpr uint64_t kDnsServerListCacheTimeoutMs     = 10 * 60 * 1000; // 10 minutes
+
     struct Transaction
     {
         otPlatDnsUpstreamQuery *mThreadTxn;
@@ -103,10 +106,12 @@
     void ForwardResponse(Transaction *aTxn);
     void CloseTransaction(Transaction *aTxn);
     void FinishTransaction(int aFd);
+    void TryRefreshDnsServerList(void);
     void LoadDnsServerListFromConf(void);
 
     int       mUpstreamDnsServerCount = 0;
     in_addr_t mUpstreamDnsServerList[kMaxUpstreamServerCount];
+    uint64_t  mUpstreamDnsServerListFreshness = 0;
 
     Transaction mUpstreamTransaction[kMaxUpstreamTransactionCount];
 };
diff --git a/src/posix/platform/spi_interface.cpp b/src/posix/platform/spi_interface.cpp
index 73ed45c..e54b6c8 100644
--- a/src/posix/platform/spi_interface.cpp
+++ b/src/posix/platform/spi_interface.cpp
@@ -85,7 +85,7 @@
 {
 }
 
-void SpiInterface::OnRcpReset(void)
+void SpiInterface::ResetStates(void)
 {
     mSpiTxIsReady         = false;
     mSpiTxRefusedCount    = 0;
@@ -95,9 +95,20 @@
     memset(mSpiTxFrameBuffer, 0, sizeof(mSpiTxFrameBuffer));
     memset(&mInterfaceMetrics, 0, sizeof(mInterfaceMetrics));
     mInterfaceMetrics.mRcpInterfaceType = OT_POSIX_RCP_BUS_SPI;
+}
 
+otError SpiInterface::HardwareReset(void)
+{
+    ResetStates();
     TriggerReset();
+
+    // If the `INT` pin is set to low during the restart of the RCP chip, which triggers continuous invalid SPI
+    // transactions by the host, it will cause the function `PushPullSpi()` to output lots of invalid warn log
+    // messages. Adding the delay here is used to wait for the RCP chip starts up to avoid outputting invalid
+    // log messages.
     usleep(static_cast<useconds_t>(mSpiResetDelay) * kUsecPerMsec);
+
+    return OT_ERROR_NONE;
 }
 
 otError SpiInterface::Init(const Url::Url &aRadioUrl)
@@ -182,12 +193,6 @@
     InitResetPin(spiGpioResetDevice, spiGpioResetLine);
     InitSpiDev(aRadioUrl.GetPath(), spiMode, spiSpeed);
 
-    // Reset RCP chip.
-    TriggerReset();
-
-    // Waiting for the RCP chip starts up.
-    usleep(static_cast<useconds_t>(spiResetDelay) * kUsecPerMsec);
-
     return OT_ERROR_NONE;
 }
 
@@ -800,6 +805,12 @@
     otError error = OT_ERROR_NONE;
 
     VerifyOrExit(aLength < (kMaxFrameSize - kSpiFrameHeaderSize), error = OT_ERROR_NO_BUFS);
+
+    if (ot::Spinel::SpinelInterface::IsSpinelResetCommand(aFrame, aLength))
+    {
+        ResetStates();
+    }
+
     VerifyOrExit(!mSpiTxIsReady, error = OT_ERROR_BUSY);
 
     memcpy(&mSpiTxFrameBuffer[kSpiFrameHeaderSize], aFrame, aLength);
diff --git a/src/posix/platform/spi_interface.hpp b/src/posix/platform/spi_interface.hpp
index 988d13f..0f3242e 100644
--- a/src/posix/platform/spi_interface.hpp
+++ b/src/posix/platform/spi_interface.hpp
@@ -147,17 +147,13 @@
     uint32_t GetBusSpeed(void) const { return ((mSpiDevFd >= 0) ? mSpiSpeedHz : 0); }
 
     /**
-     * This method is called when RCP failure detected and resets internal states of the interface.
+     * This method hardware resets the RCP.
+     *
+     * @retval OT_ERROR_NONE            Successfully reset the RCP.
+     * @retval OT_ERROR_NOT_IMPLEMENT   The hardware reset is not implemented.
      *
      */
-    void OnRcpReset(void);
-
-    /**
-     * This method is called when RCP is reset to recreate the connection with it.
-     * Intentionally empty.
-     *
-     */
-    otError ResetConnection(void) { return OT_ERROR_NONE; }
+    otError HardwareReset(void);
 
     /**
      * This method returns the RCP interface metrics.
@@ -168,6 +164,7 @@
     const otRcpInterfaceMetrics *GetRcpInterfaceMetrics(void) const { return &mInterfaceMetrics; }
 
 private:
+    void    ResetStates(void);
     int     SetupGpioHandle(int aFd, uint8_t aLine, uint32_t aHandleFlags, const char *aLabel);
     int     SetupGpioEvent(int aFd, uint8_t aLine, uint32_t aHandleFlags, uint32_t aEventFlags, const char *aLabel);
     void    SetGpioValue(int aFd, uint8_t aValue);
diff --git a/src/posix/platform/system.cpp b/src/posix/platform/system.cpp
index d74512e..6f11db7 100644
--- a/src/posix/platform/system.cpp
+++ b/src/posix/platform/system.cpp
@@ -149,9 +149,7 @@
     gNetifName[0] = '\0';
 
 #if OPENTHREAD_CONFIG_NAT64_TRANSLATOR_ENABLE
-    if ((sscanf(OPENTHREAD_POSIX_CONFIG_NAT64_CIDR, "%" SCNu8 ".%" SCNu8 ".%" SCNu8 ".%" SCNu8 "/%" SCNu8,
-                &gNat64Cidr.mAddress.mFields.m8[0], &gNat64Cidr.mAddress.mFields.m8[1],
-                &gNat64Cidr.mAddress.mFields.m8[2], &gNat64Cidr.mAddress.mFields.m8[3], &gNat64Cidr.mLength)) != 5)
+    if (otIp4CidrFromString(OPENTHREAD_POSIX_CONFIG_NAT64_CIDR, &gNat64Cidr) != OT_ERROR_NONE)
     {
         gNat64Cidr.mLength = 0;
     }
diff --git a/src/posix/platform/vendor.cmake b/src/posix/platform/vendor.cmake
new file mode 100644
index 0000000..b8f46d5
--- /dev/null
+++ b/src/posix/platform/vendor.cmake
@@ -0,0 +1,60 @@
+#
+#  Copyright (c) 2023, The OpenThread Authors.
+#  All rights reserved.
+#
+#  Redistribution and use in source and binary forms, with or without
+#  modification, are permitted provided that the following conditions are met:
+#  1. Redistributions of source code must retain the above copyright
+#     notice, this list of conditions and the following disclaimer.
+#  2. Redistributions in binary form must reproduce the above copyright
+#     notice, this list of conditions and the following disclaimer in the
+#     documentation and/or other materials provided with the distribution.
+#  3. Neither the name of the copyright holder nor the
+#     names of its contributors may be used to endorse or promote products
+#     derived from this software without specific prior written permission.
+#
+#  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+#  AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+#  IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+#  ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+#  LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+#  CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+#  SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+#  INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+#  CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+#  ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+#  POSSIBILITY OF SUCH DAMAGE.
+#
+
+set(OT_POSIX_CONFIG_RCP_VENDOR_INTERFACE "vendor_interface_example.cpp"
+    CACHE STRING "vendor interface implementation")
+
+set(OT_POSIX_CONFIG_RCP_VENDOR_DEPS_PACKAGE "" CACHE STRING
+    "name of optional external package to link to rcp vendor implementation")
+
+if(OT_POSIX_CONFIG_RCP_BUS STREQUAL "VENDOR")
+    add_library(rcp-vendor-intf ${OT_POSIX_CONFIG_RCP_VENDOR_INTERFACE})
+
+    target_link_libraries(rcp-vendor-intf PUBLIC ot-posix-config)
+
+    target_include_directories(rcp-vendor-intf
+        PUBLIC
+            ${CMAKE_CURRENT_SOURCE_DIR}
+        PRIVATE
+            ${PROJECT_SOURCE_DIR}/include
+            ${PROJECT_SOURCE_DIR}/src
+            ${PROJECT_SOURCE_DIR}/src/core
+            ${PROJECT_SOURCE_DIR}/src/posix/platform/include
+    )
+
+    target_link_libraries(openthread-posix PUBLIC rcp-vendor-intf)
+
+    if (OT_POSIX_CONFIG_RCP_VENDOR_DEPS_PACKAGE)
+        set(DEPS_TARGET ${OT_POSIX_CONFIG_RCP_VENDOR_DEPS_PACKAGE}::${OT_POSIX_CONFIG_RCP_VENDOR_DEPS_PACKAGE})
+        find_package(${OT_POSIX_CONFIG_RCP_VENDOR_DEPS_PACKAGE})
+
+        if(${OT_POSIX_CONFIG_RCP_VENDOR_DEPS_PACKAGE}_FOUND)
+            target_link_libraries(rcp-vendor-intf PUBLIC ${DEPS_TARGET})
+        endif()
+    endif()
+endif()
diff --git a/src/posix/platform/vendor_interface.hpp b/src/posix/platform/vendor_interface.hpp
index 4b2011c..8c82c20 100644
--- a/src/posix/platform/vendor_interface.hpp
+++ b/src/posix/platform/vendor_interface.hpp
@@ -144,19 +144,13 @@
     uint32_t GetBusSpeed(void) const;
 
     /**
-     * This method is called when RCP failure detected and resets internal states of the interface.
+     * This method hardware resets the RCP.
+     *
+     * @retval OT_ERROR_NONE            Successfully reset the RCP.
+     * @retval OT_ERROR_NOT_IMPLEMENT   The hardware reset is not implemented.
      *
      */
-    void OnRcpReset(void);
-
-    /**
-     * This method is called when RCP is reset to recreate the connection with it.
-     *
-     * @retval OT_ERROR_NONE    Reset the connection successfully.
-     * @retval OT_ERROR_FAILED  Failed to reset the connection.
-     *
-     */
-    otError ResetConnection(void);
+    otError HardwareReset(void);
 
     /**
      * This method returns the RCP interface metrics.
diff --git a/src/posix/platform/vendor_interface_example.cpp b/src/posix/platform/vendor_interface_example.cpp
index 6ca5f0a..ebf6b78 100644
--- a/src/posix/platform/vendor_interface_example.cpp
+++ b/src/posix/platform/vendor_interface_example.cpp
@@ -101,9 +101,11 @@
 
 uint32_t VendorInterface::GetBusSpeed(void) const { return 1000000; }
 
-void VendorInterface::OnRcpReset(void)
+otError VendorInterface::HardwareReset(void)
 {
     // TODO: Implement vendor code here.
+
+    return OT_ERROR_NOT_IMPLEMENTED;
 }
 
 void VendorInterface::UpdateFdSet(fd_set &aReadFdSet, fd_set &aWriteFdSet, int &aMaxFd, struct timeval &aTimeout)
@@ -142,13 +144,6 @@
     return OT_ERROR_NONE;
 }
 
-otError VendorInterface::ResetConnection(void)
-{
-    // TODO: Implement vendor code here.
-
-    return OT_ERROR_NONE;
-}
-
 const otRcpInterfaceMetrics *VendorInterface::GetRcpInterfaceMetrics(void)
 {
     // TODO: Implement vendor code here.
diff --git a/tests/Makefile.am b/tests/Makefile.am
index e63331a..462f908 100644
--- a/tests/Makefile.am
+++ b/tests/Makefile.am
@@ -31,7 +31,6 @@
 # Always package (e.g. for 'make dist') these subdirectories.
 
 DIST_SUBDIRS                            = \
-    scripts                               \
     fuzz                                  \
     $(NULL)
 
@@ -40,45 +39,8 @@
 SUBDIRS                                 = \
     $(NULL)
 
-if OPENTHREAD_POSIX
-if OPENTHREAD_ENABLE_CLI
-SUBDIRS                                += \
-    scripts                               \
-    $(NULL)
-endif
-endif
-
 if OPENTHREAD_ENABLE_FUZZ_TARGETS
 SUBDIRS                                += fuzz
 endif
 
-if OPENTHREAD_BUILD_TESTS
-if OPENTHREAD_BUILD_COVERAGE
-CLEANFILES                             = $(wildcard *.gcda *.gcno)
-
-if OPENTHREAD_BUILD_COVERAGE_REPORTS
-# The bundle should positively be qualified with the absolute build
-# path. Otherwise, VPATH will get auto-prefixed to it if there is
-# already such a directory in the non-colocated source tree.
-
-OPENTHREAD_COVERAGE_BUNDLE             = ${abs_builddir}/${PACKAGE}${NL_COVERAGE_BUNDLE_SUFFIX}
-OPENTHREAD_COVERAGE_INFO               = ${OPENTHREAD_COVERAGE_BUNDLE}/${PACKAGE}${NL_COVERAGE_INFO_SUFFIX}
-
-$(OPENTHREAD_COVERAGE_BUNDLE):
-	$(call create-directory)
-
-$(OPENTHREAD_COVERAGE_INFO): check | $(dir $(OPENTHREAD_COVERAGE_INFO))
-	$(call generate-coverage-report,${top_builddir})
-
-coverage-local: $(OPENTHREAD_COVERAGE_INFO)
-
-clean-local: clean-local-coverage
-
-.PHONY: clean-local-coverage
-clean-local-coverage:
-	-$(AM_V_at)rm -rf $(OPENTHREAD_COVERAGE_BUNDLE)
-endif # OPENTHREAD_BUILD_COVERAGE_REPORTS
-endif # OPENTHREAD_BUILD_COVERAGE
-endif # OPENTHREAD_BUILD_TESTS
-
 include $(abs_top_nlbuild_autotools_dir)/automake/post.am
diff --git a/tests/fuzz/oss-fuzz-build b/tests/fuzz/oss-fuzz-build
index 60782f6..c8bb407 100755
--- a/tests/fuzz/oss-fuzz-build
+++ b/tests/fuzz/oss-fuzz-build
@@ -62,8 +62,8 @@
         -DOT_LINK_RAW=ON \
         -DOT_LOG_OUTPUT=APP \
         -DOT_MAC_FILTER=ON \
-        -DOT_MTD_NETDIAG=ON \
         -DOT_NETDATA_PUBLISHER=ON \
+        -DOT_NETDIAG_CLIENT=ON \
         -DOT_PING_SENDER=ON \
         -DOT_SERVICE=ON \
         -DOT_SLAAC=ON \
diff --git a/tests/scripts/expect/cli-diags.exp b/tests/scripts/expect/cli-diags.exp
index ab4986f..e5d2fbf 100755
--- a/tests/scripts/expect/cli-diags.exp
+++ b/tests/scripts/expect/cli-diags.exp
@@ -181,6 +181,12 @@
 send "diag cw stop\n"
 expect_line "Done"
 
+send "diag stream start\n"
+expect_line "Done"
+
+send "diag stream stop\n"
+expect_line "Done"
+
 send "diag rawpowersetting 112233\n"
 expect_line "Done"
 
diff --git a/tests/scripts/expect/cli-macfilter.exp b/tests/scripts/expect/cli-macfilter.exp
index 21456b3..1827a6e 100755
--- a/tests/scripts/expect/cli-macfilter.exp
+++ b/tests/scripts/expect/cli-macfilter.exp
@@ -114,7 +114,7 @@
 expect "Address Mode: Disabled"
 expect "RssIn List:"
 expect -re {aabbccddeeff0011 : rss -?\d+ \(lqi 3\)}
-expect -re {Default rss : -?\d+ \(lqi 2\)}
+expect -re {Default rss: -?\d+ \(lqi 2\)}
 expect_line "Done"
 
 send "macfilter rss remove *\n"
diff --git a/tests/scripts/expect/cli-misc.exp b/tests/scripts/expect/cli-misc.exp
index 8b8b4d5..522a0fa 100755
--- a/tests/scripts/expect/cli-misc.exp
+++ b/tests/scripts/expect/cli-misc.exp
@@ -86,6 +86,10 @@
 expect "up"
 expect_line "Done"
 
+send "instanceid\n"
+expect -re {\d+}
+expect_line "Done"
+
 send "ipaddr add ::\n"
 expect_line "Done"
 send "ipaddr del ::\n"
diff --git a/tests/scripts/expect/tun-dns-over-tcp-client.exp b/tests/scripts/expect/tun-dns-over-tcp-client.exp
index 3e74bfd..3da428a 100755
--- a/tests/scripts/expect/tun-dns-over-tcp-client.exp
+++ b/tests/scripts/expect/tun-dns-over-tcp-client.exp
@@ -39,7 +39,7 @@
 set addr_1 [get_ipaddr mleid]
 
 switch_node 2
-send "dns resolve ipv6.google.com $addr_1 2000 6000 4 1 tcp\n"
+send "dns resolve ipv6.google.com $addr_1 2000 6000 4 1 def tcp\n"
 expect "DNS response for ipv6.google.com"
 expect_line "Done"
 
diff --git a/tests/scripts/thread-cert/Cert_5_3_03_AddressQuery.py b/tests/scripts/thread-cert/Cert_5_3_03_AddressQuery.py
index 233764d..403a397 100755
--- a/tests/scripts/thread-cert/Cert_5_3_03_AddressQuery.py
+++ b/tests/scripts/thread-cert/Cert_5_3_03_AddressQuery.py
@@ -118,7 +118,7 @@
         self.assertTrue(self.nodes[MED1].ping(router3_mleid))
 
         # 3
-        # Wait the finish of address resolution traffic triggerred by previous
+        # Wait the finish of address resolution traffic triggered by previous
         # ping.
         self.simulator.go(5)
 
@@ -126,7 +126,7 @@
         self.assertTrue(self.nodes[ROUTER1].ping(med1_mleid))
 
         # 4
-        # Wait the finish of address resolution traffic triggerred by previous
+        # Wait the finish of address resolution traffic triggered by previous
         # ping.
         self.simulator.go(5)
 
diff --git a/tests/scripts/thread-cert/Cert_5_3_09_AddressQuery.py b/tests/scripts/thread-cert/Cert_5_3_09_AddressQuery.py
index 6e1ca9d..74608b3 100755
--- a/tests/scripts/thread-cert/Cert_5_3_09_AddressQuery.py
+++ b/tests/scripts/thread-cert/Cert_5_3_09_AddressQuery.py
@@ -155,7 +155,7 @@
         self.assertTrue(self.nodes[SED1].ping(router3_addr))
         self.simulator.go(1)
 
-        # 6 DUT_ROUTER2: Power off ROUTER3 and wait 580s to alow LEADER to
+        # 6 DUT_ROUTER2: Power off ROUTER3 and wait 580s to allow LEADER to
         # expire its Router ID
         self.nodes[ROUTER3].stop()
         self.simulator.go(580)
@@ -233,7 +233,7 @@
             must_next()
 
         # Step 3: Router_1 sends an ICMPv6 Echo Request to SED using GUA 2001::
-        #         addresss
+        #         address
         #         The DUT MUST respond to the Address Query Request with a properly
         #         formatted Address Notification Message:
         #             CoAP URI-Path
diff --git a/tests/scripts/thread-cert/Cert_5_3_10_AddressQuery.py b/tests/scripts/thread-cert/Cert_5_3_10_AddressQuery.py
index 775ee5f..8a63548 100755
--- a/tests/scripts/thread-cert/Cert_5_3_10_AddressQuery.py
+++ b/tests/scripts/thread-cert/Cert_5_3_10_AddressQuery.py
@@ -239,7 +239,7 @@
             must_next()
 
         # Step 4: Border Router sends an ICMPv6 Echo Request to MED using GUA 2003::
-        #         addresss
+        #         address
         #         The DUT MUST respond to the Address Query Request with a properly
         #         formatted Address Notification Message:
         #             CoAP URI-Path
diff --git a/tests/scripts/thread-cert/Cert_5_7_03_CoapDiagCommands.py b/tests/scripts/thread-cert/Cert_5_7_03_CoapDiagCommands.py
index 36b5b98..bf24826 100755
--- a/tests/scripts/thread-cert/Cert_5_7_03_CoapDiagCommands.py
+++ b/tests/scripts/thread-cert/Cert_5_7_03_CoapDiagCommands.py
@@ -175,7 +175,7 @@
         #             TLV Type 8 – IPv6 address list
         #             TLV Type 17 – Channel Pagesi
         #
-        #         if DUT is Router, contianing the following as well:
+        #         if DUT is Router, containing the following as well:
         #             TLV Type 4 – Connectivity
         #             TLV Type 5 – Route64
         #             TLV Type 16 – Child Table
diff --git a/tests/scripts/thread-cert/Cert_5_8_04_SecurityPolicyTLV.py b/tests/scripts/thread-cert/Cert_5_8_04_SecurityPolicyTLV.py
index 37f5506..8620314 100755
--- a/tests/scripts/thread-cert/Cert_5_8_04_SecurityPolicyTLV.py
+++ b/tests/scripts/thread-cert/Cert_5_8_04_SecurityPolicyTLV.py
@@ -49,7 +49,7 @@
 # requires an External Commissioner which is currently not part of Thread
 # Certification.
 #
-# Notes: Due to the packet parsing compatiable issue for supporting Thread 1.2
+# Notes: Due to the packet parsing compatible issue for supporting Thread 1.2
 #        and 1.1, the security policy values can be fetched only in the unknown
 #        field.
 #
diff --git a/tests/scripts/thread-cert/Makefile.am b/tests/scripts/thread-cert/Makefile.am
deleted file mode 100644
index a3d9970..0000000
--- a/tests/scripts/thread-cert/Makefile.am
+++ /dev/null
@@ -1,403 +0,0 @@
-#
-#  Copyright (c) 2016-2017, The OpenThread Authors.
-#  All rights reserved.
-#
-#  Redistribution and use in source and binary forms, with or without
-#  modification, are permitted provided that the following conditions are met:
-#  1. Redistributions of source code must retain the above copyright
-#     notice, this list of conditions and the following disclaimer.
-#  2. Redistributions in binary form must reproduce the above copyright
-#     notice, this list of conditions and the following disclaimer in the
-#     documentation and/or other materials provided with the distribution.
-#  3. Neither the name of the copyright holder nor the
-#     names of its contributors may be used to endorse or promote products
-#     derived from this software without specific prior written permission.
-#
-#  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
-#  AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
-#  IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
-#  ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
-#  LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
-#  CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
-#  SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
-#  INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
-#  CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
-#  ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
-#  POSSIBILITY OF SUCH DAMAGE.
-#
-
-include $(abs_top_nlbuild_autotools_dir)/automake/pre.am
-
-LOG_DRIVER=$(abs_top_srcdir)/third_party/openthread-test-driver/test-driver
-
-EXTRA_DIST                                                         = \
-    Cert_5_1_01_RouterAttach.py                                      \
-    Cert_5_1_02_ChildAddressTimeout.py                               \
-    Cert_5_1_03_RouterAddressReallocation.py                         \
-    Cert_5_1_04_RouterAddressReallocation.py                         \
-    Cert_5_1_05_RouterAddressTimeout.py                              \
-    Cert_5_1_06_RemoveRouterId.py                                    \
-    Cert_5_1_07_MaxChildCount.py                                     \
-    Cert_5_1_08_RouterAttachConnectivity.py                          \
-    Cert_5_1_09_REEDAttachConnectivity.py                            \
-    Cert_5_1_10_RouterAttachLinkQuality.py                           \
-    Cert_5_1_11_REEDAttachLinkQuality.py                             \
-    Cert_5_1_12_NewRouterNeighborSync.py                             \
-    Cert_5_1_13_RouterReset.py                                       \
-    Cert_5_2_01_REEDAttach.py                                        \
-    Cert_5_2_03_LeaderReject2Hops.py                                 \
-    Cert_5_2_04_REEDUpgrade.py                                       \
-    Cert_5_2_05_AddressQuery.py                                      \
-    Cert_5_2_06_RouterDowngrade.py                                   \
-    Cert_5_2_07_REEDSynchronization.py                               \
-    Cert_5_3_01_LinkLocal.py                                         \
-    Cert_5_3_02_RealmLocal.py                                        \
-    Cert_5_3_03_AddressQuery.py                                      \
-    Cert_5_3_04_AddressMapCache.py                                   \
-    Cert_5_3_05_RoutingLinkQuality.py                                \
-    Cert_5_3_06_RouterIdMask.py                                      \
-    Cert_5_3_07_DuplicateAddress.py                                  \
-    Cert_5_3_08_ChildAddressSet.py                                   \
-    Cert_5_3_09_AddressQuery.py                                      \
-    Cert_5_3_10_AddressQuery.py                                      \
-    Cert_5_3_11_AddressQueryTimeoutIntervals.py                      \
-    Cert_5_5_01_LeaderReboot.py                                      \
-    Cert_5_5_02_LeaderReboot.py                                      \
-    Cert_5_5_03_SplitMergeChildren.py                                \
-    Cert_5_5_04_SplitMergeRouters.py                                 \
-    Cert_5_5_05_SplitMergeREED.py                                    \
-    Cert_5_5_07_SplitMergeThreeWay.py                                \
-    Cert_5_6_01_NetworkDataRegisterBeforeAttachLeader.py             \
-    Cert_5_6_02_NetworkDataRegisterBeforeAttachRouter.py             \
-    Cert_5_6_03_NetworkDataRegisterAfterAttachLeader.py              \
-    Cert_5_6_04_NetworkDataRegisterAfterAttachRouter.py              \
-    Cert_5_6_05_NetworkDataRegisterAfterAttachRouter.py              \
-    Cert_5_6_06_NetworkDataExpiration.py                             \
-    Cert_5_6_07_NetworkDataRequestREED.py                            \
-    Cert_5_6_09_NetworkDataForwarding.py                             \
-    Cert_5_7_01_CoapDiagCommands.py                                  \
-    Cert_5_7_02_CoapDiagCommands.py                                  \
-    Cert_5_7_03_CoapDiagCommands.py                                  \
-    Cert_5_8_02_KeyIncrement.py                                      \
-    Cert_5_8_03_KeyIncrementRollOver.py                              \
-    Cert_5_8_04_SecurityPolicyTLV.py                                 \
-    Cert_6_1_01_RouterAttach.py                                      \
-    Cert_6_1_02_REEDAttach.py                                        \
-    Cert_6_1_03_RouterAttachConnectivity.py                          \
-    Cert_6_1_04_REEDAttachConnectivity.py                            \
-    Cert_6_1_05_REEDAttachConnectivity.py                            \
-    Cert_6_1_07_RouterAttachLinkQuality.py                           \
-    Cert_6_1_06_REEDAttachLinkQuality.py                             \
-    Cert_6_2_01_NewPartition.py                                      \
-    Cert_6_2_02_NewPartition.py                                      \
-    Cert_6_3_01_OrphanReattach.py                                    \
-    Cert_6_3_02_NetworkDataUpdate.py                                 \
-    Cert_6_4_01_LinkLocal.py                                         \
-    Cert_6_4_02_RealmLocal.py                                        \
-    Cert_6_5_01_ChildResetReattach.py                                \
-    Cert_6_5_02_ChildResetReattach.py                                \
-    Cert_6_5_03_ChildResetSynchronize.py                             \
-    Cert_6_6_01_KeyIncrement.py                                      \
-    Cert_6_6_02_KeyIncrementRollOver.py                              \
-    Cert_7_1_01_BorderRouterAsLeader.py                              \
-    Cert_7_1_02_BorderRouterAsRouter.py                              \
-    Cert_7_1_03_BorderRouterAsLeader.py                              \
-    Cert_7_1_04_BorderRouterAsRouter.py                              \
-    Cert_7_1_05_BorderRouterAsRouter.py                              \
-    Cert_7_1_06_BorderRouterAsLeader.py                              \
-    Cert_7_1_07_BorderRouterAsLeader.py                              \
-    Cert_7_1_08_BorderRouterAsFED.py                                 \
-    Cert_8_1_01_Commissioning.py                                     \
-    Cert_8_1_02_Commissioning.py                                     \
-    Cert_8_2_01_JoinerRouter.py                                      \
-    Cert_8_2_02_JoinerRouter.py                                      \
-    Cert_9_2_01_MGMTCommissionerGet.py                               \
-    Cert_9_2_02_MGMTCommissionerSet.py                               \
-    Cert_9_2_03_ActiveDatasetGet.py                                  \
-    Cert_9_2_04_ActiveDataset.py                                     \
-    Cert_9_2_05_ActiveDataset.py                                     \
-    Cert_9_2_06_DatasetDissemination.py                              \
-    Cert_9_2_07_DelayTimer.py                                        \
-    Cert_9_2_08_PersistentDatasets.py                                \
-    Cert_9_2_09_PendingPartition.py                                  \
-    Cert_9_2_10_PendingPartition.py                                  \
-    Cert_9_2_11_NetworkKey.py                                        \
-    Cert_9_2_12_Announce.py                                          \
-    Cert_9_2_13_EnergyScan.py                                        \
-    Cert_9_2_14_PanIdQuery.py                                        \
-    Cert_9_2_15_PendingPartition.py                                  \
-    Cert_9_2_16_ActivePendingPartition.py                            \
-    Cert_9_2_17_Orphan.py                                            \
-    Cert_9_2_18_RollBackActiveTimestamp.py                           \
-    Cert_9_2_19_PendingDatasetGet.py                                 \
-    coap.py                                                          \
-    command.py                                                       \
-    common.py                                                        \
-    config.py                                                        \
-    debug.py                                                         \
-    dtls.py                                                          \
-    ipv6.py                                                          \
-    lowpan.py                                                        \
-    mac802154.py                                                     \
-    mesh_cop.py                                                      \
-    message.py                                                       \
-    mle.py                                                           \
-    net_crypto.py                                                    \
-    network_data.py                                                  \
-    network_diag.py                                                  \
-    network_layer.py                                                 \
-    node.py                                                          \
-    pcap.py                                                          \
-    simulator.py                                                     \
-    sniffer.py                                                       \
-    sniffer_transport.py                                             \
-    test_anycast.py                                                  \
-    test_anycast_locator.py                                          \
-    test_br_upgrade_router_role.py                                   \
-    test_child_supervision.py                                        \
-    test_coap.py                                                     \
-    test_coap_block.py                                               \
-    test_coap_observe.py                                             \
-    test_coaps.py                                                    \
-    test_common.py                                                   \
-    test_crypto.py                                                   \
-    test_dataset_updater.py                                          \
-    test_detach.py                                                   \
-    test_diag.py                                                     \
-    test_dns_client_config_auto_start.py                             \
-    test_dnssd.py                                                    \
-    test_dnssd_name_with_special_chars.py                            \
-    test_history_tracker.py                                          \
-    test_inform_previous_parent_on_reattach.py                       \
-    test_ipv6.py                                                     \
-    test_ipv6_fragmentation.py                                       \
-    test_ipv6_source_selection.py                                    \
-    test_lowpan.py                                                   \
-    test_mac802154.py                                                \
-    test_mac_scan.py                                                 \
-    test_mle.py                                                      \
-    test_mle_msg_key_seq_jump.py                                     \
-    test_netdata_publisher.py                                        \
-    test_network_data.py                                             \
-    test_network_layer.py                                            \
-    test_on_mesh_prefix.py                                           \
-    test_pbbr_aloc.py                                                \
-    test_ping.py                                                     \
-    test_radio_filter.py                                             \
-    test_reed_address_solicit_rejected.py                            \
-    test_reset.py                                                    \
-    test_route_table.py                                              \
-    test_router_reattach.py                                          \
-    test_router_upgrade.py                                           \
-    test_service.py                                                  \
-    test_set_mliid.py                                                \
-    test_srp_auto_host_address.py                                    \
-    test_srp_auto_start_mode.py                                      \
-    test_srp_client_remove_host.py                                   \
-    test_srp_client_save_server_info.py                              \
-    test_srp_lease.py                                                \
-    test_srp_many_services_mtu_check.py                              \
-    test_srp_name_conflicts.py                                       \
-    test_srp_register_single_service.py                              \
-    test_srp_register_services_diff_lease.py                         \
-    test_srp_server_anycast_mode.py                                  \
-    test_srp_server_reboot_port.py                                   \
-    test_srp_sub_type.py                                             \
-    test_srp_ttl.py                                                  \
-    test_zero_len_external_route.py                                  \
-    thread_cert.py                                                   \
-    tlvs_parsing.py                                                  \
-    thread_cert.py                                                   \
-    pktverify/__init__.py                                            \
-    pktverify/addrs.py                                               \
-    pktverify/bytes.py                                               \
-    pktverify/coap.py                                                \
-    pktverify/consts.py                                              \
-    pktverify/decorators.py                                          \
-    pktverify/errors.py                                              \
-    pktverify/layer_fields.py                                        \
-    pktverify/layer_fields_container.py                              \
-    pktverify/layers.py                                              \
-    pktverify/null_field.py                                          \
-    pktverify/packet.py                                              \
-    pktverify/packet_filter.py                                       \
-    pktverify/packet_verifier.py                                     \
-    pktverify/pcap_reader.py                                         \
-    pktverify/summary.py                                             \
-    pktverify/test_info.py                                           \
-    pktverify/utils.py                                               \
-    pktverify/verify_result.py                                       \
-    wpan.py                                                          \
-    $(NULL)
-
-check_PROGRAMS                                                     = \
-    $(NULL)
-
-check_SCRIPTS                                                      = \
-    test_anycast.py                                                  \
-    test_anycast_locator.py                                          \
-    test_br_upgrade_router_role.py                                   \
-    test_child_supervision.py                                        \
-    test_coap.py                                                     \
-    test_coap_block.py                                               \
-    test_coap_observe.py                                             \
-    test_coaps.py                                                    \
-    test_common.py                                                   \
-    test_crypto.py                                                   \
-    test_dataset_updater.py                                          \
-    test_detach.py                                                   \
-    test_diag.py                                                     \
-    test_dns_client_config_auto_start.py                             \
-    test_dnssd.py                                                    \
-    test_dnssd_name_with_special_chars.py                            \
-    test_history_tracker.py                                          \
-    test_inform_previous_parent_on_reattach.py                       \
-    test_ipv6.py                                                     \
-    test_ipv6_fragmentation.py                                       \
-    test_ipv6_source_selection.py                                    \
-    test_lowpan.py                                                   \
-    test_mac802154.py                                                \
-    test_mac_scan.py                                                 \
-    test_mle.py                                                      \
-    test_mle_msg_key_seq_jump.py                                     \
-    test_netdata_publisher.py                                        \
-    test_network_data.py                                             \
-    test_network_layer.py                                            \
-    test_on_mesh_prefix.py                                           \
-    test_pbbr_aloc.py                                                \
-    test_ping.py                                                     \
-    test_radio_filter.py                                             \
-    test_reed_address_solicit_rejected.py                            \
-    test_reset.py                                                    \
-    test_route_table.py                                              \
-    test_router_reattach.py                                          \
-    test_router_upgrade.py                                           \
-    test_service.py                                                  \
-    test_srp_auto_host_address.py                                    \
-    test_srp_auto_start_mode.py                                      \
-    test_srp_client_remove_host.py                                   \
-    test_srp_client_save_server_info.py                              \
-    test_srp_lease.py                                                \
-    test_srp_many_services_mtu_check.py                              \
-    test_srp_name_conflicts.py                                       \
-    test_srp_register_single_service.py                              \
-    test_srp_register_services_diff_lease.py                         \
-    test_srp_server_anycast_mode.py                                  \
-    test_srp_server_reboot_port.py                                   \
-    test_srp_sub_type.py                                             \
-    test_srp_ttl.py                                                  \
-    test_zero_len_external_route.py                                  \
-    Cert_5_1_01_RouterAttach.py                                      \
-    Cert_5_1_02_ChildAddressTimeout.py                               \
-    Cert_5_1_03_RouterAddressReallocation.py                         \
-    Cert_5_1_04_RouterAddressReallocation.py                         \
-    Cert_5_1_05_RouterAddressTimeout.py                              \
-    Cert_5_1_06_RemoveRouterId.py                                    \
-    Cert_5_1_07_MaxChildCount.py                                     \
-    Cert_5_1_08_RouterAttachConnectivity.py                          \
-    Cert_5_1_09_REEDAttachConnectivity.py                            \
-    Cert_5_1_10_RouterAttachLinkQuality.py                           \
-    Cert_5_1_11_REEDAttachLinkQuality.py                             \
-    Cert_5_1_12_NewRouterNeighborSync.py                             \
-    Cert_5_1_13_RouterReset.py                                       \
-    Cert_5_2_01_REEDAttach.py                                        \
-    Cert_5_2_05_AddressQuery.py                                      \
-    Cert_5_2_06_RouterDowngrade.py                                   \
-    Cert_5_2_07_REEDSynchronization.py                               \
-    Cert_5_2_04_REEDUpgrade.py                                       \
-    Cert_5_3_01_LinkLocal.py                                         \
-    Cert_5_3_02_RealmLocal.py                                        \
-    Cert_5_3_03_AddressQuery.py                                      \
-    Cert_5_3_04_AddressMapCache.py                                   \
-    Cert_5_3_05_RoutingLinkQuality.py                                \
-    Cert_5_3_06_RouterIdMask.py                                      \
-    Cert_5_3_07_DuplicateAddress.py                                  \
-    Cert_5_3_08_ChildAddressSet.py                                   \
-    Cert_5_3_09_AddressQuery.py                                      \
-    Cert_5_3_10_AddressQuery.py                                      \
-    Cert_5_3_11_AddressQueryTimeoutIntervals.py                      \
-    Cert_5_5_01_LeaderReboot.py                                      \
-    Cert_5_5_02_LeaderReboot.py                                      \
-    Cert_5_5_03_SplitMergeChildren.py                                \
-    Cert_5_5_04_SplitMergeRouters.py                                 \
-    Cert_5_5_05_SplitMergeREED.py                                    \
-    Cert_5_5_07_SplitMergeThreeWay.py                                \
-    Cert_5_6_01_NetworkDataRegisterBeforeAttachLeader.py             \
-    Cert_5_6_02_NetworkDataRegisterBeforeAttachRouter.py             \
-    Cert_5_6_03_NetworkDataRegisterAfterAttachLeader.py              \
-    Cert_5_6_04_NetworkDataRegisterAfterAttachRouter.py              \
-    Cert_5_6_05_NetworkDataRegisterAfterAttachRouter.py              \
-    Cert_5_6_06_NetworkDataExpiration.py                             \
-    Cert_5_6_07_NetworkDataRequestREED.py                            \
-    Cert_5_6_09_NetworkDataForwarding.py                             \
-    Cert_5_7_01_CoapDiagCommands.py                                  \
-    Cert_5_7_02_CoapDiagCommands.py                                  \
-    Cert_5_7_03_CoapDiagCommands.py                                  \
-    Cert_5_8_02_KeyIncrement.py                                      \
-    Cert_5_8_03_KeyIncrementRollOver.py                              \
-    Cert_5_8_04_SecurityPolicyTLV.py                                 \
-    Cert_6_1_01_RouterAttach.py                                      \
-    Cert_6_1_02_REEDAttach.py                                        \
-    Cert_6_1_03_RouterAttachConnectivity.py                          \
-    Cert_6_1_04_REEDAttachConnectivity.py                            \
-    Cert_6_1_05_REEDAttachConnectivity.py                            \
-    Cert_6_1_06_REEDAttachLinkQuality.py                             \
-    Cert_6_1_07_RouterAttachLinkQuality.py                           \
-    Cert_6_2_01_NewPartition.py                                      \
-    Cert_6_2_02_NewPartition.py                                      \
-    Cert_6_3_01_OrphanReattach.py                                    \
-    Cert_6_3_02_NetworkDataUpdate.py                                 \
-    Cert_6_4_01_LinkLocal.py                                         \
-    Cert_6_4_02_RealmLocal.py                                        \
-    Cert_6_5_01_ChildResetReattach.py                                \
-    Cert_6_5_02_ChildResetReattach.py                                \
-    Cert_6_5_03_ChildResetSynchronize.py                             \
-    Cert_6_6_01_KeyIncrement.py                                      \
-    Cert_6_6_02_KeyIncrementRollOver.py                              \
-    Cert_5_2_03_LeaderReject2Hops.py                                 \
-    Cert_7_1_01_BorderRouterAsLeader.py                              \
-    Cert_7_1_02_BorderRouterAsRouter.py                              \
-    Cert_7_1_03_BorderRouterAsLeader.py                              \
-    Cert_7_1_04_BorderRouterAsRouter.py                              \
-    Cert_7_1_05_BorderRouterAsRouter.py                              \
-    Cert_7_1_06_BorderRouterAsLeader.py                              \
-    Cert_7_1_07_BorderRouterAsLeader.py                              \
-    Cert_7_1_08_BorderRouterAsFED.py                                 \
-    Cert_8_1_01_Commissioning.py                                     \
-    Cert_8_1_02_Commissioning.py                                     \
-    Cert_8_2_01_JoinerRouter.py                                      \
-    Cert_8_2_02_JoinerRouter.py                                      \
-    Cert_9_2_01_MGMTCommissionerGet.py                               \
-    Cert_9_2_02_MGMTCommissionerSet.py                               \
-    Cert_9_2_03_ActiveDatasetGet.py                                  \
-    Cert_9_2_04_ActiveDataset.py                                     \
-    Cert_9_2_05_ActiveDataset.py                                     \
-    Cert_9_2_06_DatasetDissemination.py                              \
-    Cert_9_2_07_DelayTimer.py                                        \
-    Cert_9_2_08_PersistentDatasets.py                                \
-    Cert_9_2_09_PendingPartition.py                                  \
-    Cert_9_2_10_PendingPartition.py                                  \
-    Cert_9_2_11_NetworkKey.py                                        \
-    Cert_9_2_12_Announce.py                                          \
-    Cert_9_2_13_EnergyScan.py                                        \
-    Cert_9_2_14_PanIdQuery.py                                        \
-    Cert_9_2_15_PendingPartition.py                                  \
-    Cert_9_2_16_ActivePendingPartition.py                            \
-    Cert_9_2_17_Orphan.py                                            \
-    Cert_9_2_18_RollBackActiveTimestamp.py                           \
-    Cert_9_2_19_PendingDatasetGet.py                                 \
-    $(NULL)
-
-TESTS_ENVIRONMENT                                                  = \
-    export                                                           \
-    top_builddir='$(top_builddir)'                                   \
-    top_srcdir='$(top_srcdir)'                                       \
-    VERBOSE=1;                                                       \
-    $(NULL)
-
-TESTS                                                              = \
-    $(check_PROGRAMS)                                                \
-    $(check_SCRIPTS)                                                 \
-    $(NULL)
-
-include $(abs_top_nlbuild_autotools_dir)/automake/post.am
diff --git a/tests/scripts/thread-cert/__init__.py b/tests/scripts/thread-cert/__init__.py
old mode 100755
new mode 100644
diff --git a/tests/scripts/thread-cert/backbone/test_bmlr.py b/tests/scripts/thread-cert/backbone/test_bmlr.py
old mode 100644
new mode 100755
index 990038c..335d905
--- a/tests/scripts/thread-cert/backbone/test_bmlr.py
+++ b/tests/scripts/thread-cert/backbone/test_bmlr.py
@@ -161,7 +161,7 @@
             and ipv6.src.is_link_local
         """)
 
-        # Commissioner registers MA3 with deafult timeout
+        # Commissioner registers MA3 with default timeout
         pkts.filter_wpan_src64(COMMISSIONER).filter_coap_request('/n/mr').must_next().must_verify(f"""
             thread_meshcop.tlv.ipv6_addr == ['{MA3}']
             and thread_bl.tlv.timeout is null
diff --git a/tests/scripts/thread-cert/backbone/test_dua_dad.py b/tests/scripts/thread-cert/backbone/test_dua_dad.py
old mode 100644
new mode 100755
diff --git a/tests/scripts/thread-cert/backbone/test_dua_routing.py b/tests/scripts/thread-cert/backbone/test_dua_routing.py
old mode 100644
new mode 100755
index 3c3c249..fd469e1
--- a/tests/scripts/thread-cert/backbone/test_dua_routing.py
+++ b/tests/scripts/thread-cert/backbone/test_dua_routing.py
@@ -211,7 +211,7 @@
         PBBR_ETH = pv.vars['PBBR_ETH']
         PBBR2_ETH = pv.vars['PBBR2_ETH']
 
-        # Verify that SBBR should not foward any Ping Request to the Thread network.
+        # Verify that SBBR should not forward any Ping Request to the Thread network.
         # Use `ipv6.hlim == 63` to avoid false fails because SBBR might still forward Ping Request from PBBR to ROUTER1
         pkts.filter_wpan_src64(SBBR).filter_ping_request().filter('ipv6.hlim == 63').must_not_next()
 
diff --git a/tests/scripts/thread-cert/backbone/test_dua_routing_med.py b/tests/scripts/thread-cert/backbone/test_dua_routing_med.py
old mode 100644
new mode 100755
diff --git a/tests/scripts/thread-cert/backbone/test_mle_must_not_send_icmpv6_destination_unreachable.py b/tests/scripts/thread-cert/backbone/test_mle_must_not_send_icmpv6_destination_unreachable.py
old mode 100644
new mode 100755
diff --git a/tests/scripts/thread-cert/backbone/test_mlr_multicast_routing.py b/tests/scripts/thread-cert/backbone/test_mlr_multicast_routing.py
old mode 100644
new mode 100755
diff --git a/tests/scripts/thread-cert/backbone/test_mlr_multicast_routing_across_thread_pans.py b/tests/scripts/thread-cert/backbone/test_mlr_multicast_routing_across_thread_pans.py
old mode 100644
new mode 100755
diff --git a/tests/scripts/thread-cert/backbone/test_mlr_multicast_routing_commissioner_timeout.py b/tests/scripts/thread-cert/backbone/test_mlr_multicast_routing_commissioner_timeout.py
old mode 100644
new mode 100755
diff --git a/tests/scripts/thread-cert/backbone/test_mlr_multicast_routing_timeout.py b/tests/scripts/thread-cert/backbone/test_mlr_multicast_routing_timeout.py
old mode 100644
new mode 100755
diff --git a/tests/scripts/thread-cert/backbone/test_ndproxy.py b/tests/scripts/thread-cert/backbone/test_ndproxy.py
old mode 100644
new mode 100755
diff --git a/tests/scripts/thread-cert/border_router/LowPower/v1_2_LowPower_5_3_01_SSEDAttachment_BR.py b/tests/scripts/thread-cert/border_router/LowPower/v1_2_LowPower_5_3_01_SSEDAttachment_BR.py
old mode 100644
new mode 100755
diff --git a/tests/scripts/thread-cert/border_router/MATN/MATN_02_MLRFirstUse.py b/tests/scripts/thread-cert/border_router/MATN/MATN_02_MLRFirstUse.py
old mode 100644
new mode 100755
diff --git a/tests/scripts/thread-cert/border_router/MATN/MATN_03_InvalidCommissionerDeregistration.py b/tests/scripts/thread-cert/border_router/MATN/MATN_03_InvalidCommissionerDeregistration.py
old mode 100644
new mode 100755
diff --git a/tests/scripts/thread-cert/border_router/MATN/MATN_04_MulticastListenerTimeout.py b/tests/scripts/thread-cert/border_router/MATN/MATN_04_MulticastListenerTimeout.py
old mode 100644
new mode 100755
diff --git a/tests/scripts/thread-cert/border_router/MATN/MATN_05_ReregistrationToSameMulticastGroup.py b/tests/scripts/thread-cert/border_router/MATN/MATN_05_ReregistrationToSameMulticastGroup.py
old mode 100644
new mode 100755
diff --git a/tests/scripts/thread-cert/border_router/MATN/MATN_09_DefaultBRMulticastForwarding.py b/tests/scripts/thread-cert/border_router/MATN/MATN_09_DefaultBRMulticastForwarding.py
old mode 100644
new mode 100755
diff --git a/tests/scripts/thread-cert/border_router/MATN/MATN_12_HopLimitProcessing.py b/tests/scripts/thread-cert/border_router/MATN/MATN_12_HopLimitProcessing.py
old mode 100644
new mode 100755
diff --git a/tests/scripts/thread-cert/border_router/MATN/MATN_15_ChangeOfPrimaryBBRTriggersRegistration.py b/tests/scripts/thread-cert/border_router/MATN/MATN_15_ChangeOfPrimaryBBRTriggersRegistration.py
old mode 100644
new mode 100755
diff --git a/tests/scripts/thread-cert/border_router/MATN/MATN_16_LargeNumberOfMulticastGroupSubscriptionsToBBR.py b/tests/scripts/thread-cert/border_router/MATN/MATN_16_LargeNumberOfMulticastGroupSubscriptionsToBBR.py
old mode 100644
new mode 100755
diff --git a/tests/scripts/thread-cert/border_router/nat64/test_multi_border_routers.py b/tests/scripts/thread-cert/border_router/nat64/test_multi_border_routers.py
old mode 100644
new mode 100755
index 7a4791d..46896bc
--- a/tests/scripts/thread-cert/border_router/nat64/test_multi_border_routers.py
+++ b/tests/scripts/thread-cert/border_router/nat64/test_multi_border_routers.py
@@ -49,6 +49,8 @@
 BR2 = 3
 HOST = 4
 
+OMR_PREFIX = "2000:0:1111:4444::/64"
+
 NAT64_PREFIX_REFRESH_DELAY = 305
 
 NAT64_STATE_DISABLED = 'disabled'
@@ -118,9 +120,15 @@
         self.simulator.go(config.BORDER_ROUTER_STARTUP_DELAY)
         self.assertEqual('router', br2.get_state())
 
+        br2.add_prefix(OMR_PREFIX)
+        br2.register_netdata()
+        self.simulator.go(10)
+
         self.simulator.go(10)
         self.assertNotEqual(br1.get_br_favored_nat64_prefix(), br2.get_br_favored_nat64_prefix())
         br1_local_nat64_prefix = br1.get_br_nat64_prefix()
+        br2_local_nat64_prefix = br2.get_br_nat64_prefix()
+        self.assertNotEqual(br2_local_nat64_prefix, br2.get_br_favored_nat64_prefix())
         br2_infra_nat64_prefix = br2.get_br_favored_nat64_prefix()
 
         self.assertEqual(len(br1.get_netdata_nat64_prefix()), 1)
@@ -164,8 +172,7 @@
         br2.nat64_set_enabled(True)
 
         self.simulator.go(10)
-        self.assertNotEqual(br2_infra_nat64_prefix, br2.get_br_favored_nat64_prefix())
-        br2_local_nat64_prefix = br2.get_br_nat64_prefix()
+        self.assertEqual(br2_local_nat64_prefix, br2.get_br_favored_nat64_prefix())
 
         self.assertEqual(len(br1.get_netdata_nat64_prefix()), 1)
         nat64_prefix = br1.get_netdata_nat64_prefix()[0]
diff --git a/tests/scripts/thread-cert/border_router/nat64/test_single_border_router.py b/tests/scripts/thread-cert/border_router/nat64/test_single_border_router.py
old mode 100644
new mode 100755
index 4a69b2a..7aeea62
--- a/tests/scripts/thread-cert/border_router/nat64/test_single_border_router.py
+++ b/tests/scripts/thread-cert/border_router/nat64/test_single_border_router.py
@@ -99,7 +99,7 @@
         if ready[0]:
             return sock.recv(1024)
         else:
-            raise AssertionError("No data recevied")
+            raise AssertionError("No data received")
 
     def listen_udp(self, addr, port):
         sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
diff --git a/tests/scripts/thread-cert/border_router/nat64/test_upstream_dns.py b/tests/scripts/thread-cert/border_router/nat64/test_upstream_dns.py
old mode 100644
new mode 100755
diff --git a/tests/scripts/thread-cert/border_router/nat64/test_with_infrastructure_prefix.py b/tests/scripts/thread-cert/border_router/nat64/test_with_infrastructure_prefix.py
old mode 100644
new mode 100755
index 465b5a7..905e6f2
--- a/tests/scripts/thread-cert/border_router/nat64/test_with_infrastructure_prefix.py
+++ b/tests/scripts/thread-cert/border_router/nat64/test_with_infrastructure_prefix.py
@@ -47,6 +47,7 @@
 BR = 1
 ROUTER = 2
 
+OMR_PREFIX = "2000:0:1111:4444::/64"
 # The prefix is set smaller than the default infrastructure NAT64 prefix.
 SMALL_NAT64_PREFIX = "2000:0:0:1:0:0::/96"
 
@@ -90,8 +91,26 @@
         self.simulator.go(config.ROUTER_STARTUP_DELAY)
         self.assertEqual('router', router.get_state())
 
-        # Case 1 BR advertise the infrastructure prefix
-        infra_nat64_prefix = br.get_br_favored_nat64_prefix()
+        # Case 1 No infra-derived OMR prefix. BR publishes its local prefix.
+        local_nat64_prefix = br.get_br_nat64_prefix()
+
+        self.assertEqual(len(br.get_netdata_nat64_prefix()), 1)
+        nat64_prefix = br.get_netdata_nat64_prefix()[0]
+        self.assertEqual(nat64_prefix, local_nat64_prefix)
+
+        self.assertDictIncludes(br.nat64_state, {
+            'PrefixManager': NAT64_STATE_ACTIVE,
+            'Translator': NAT64_STATE_ACTIVE
+        })
+
+        # Case 2 Add OMR prefix. BR publishes the infrastructure nat64 prefix
+        br.add_prefix(OMR_PREFIX)
+        br.register_netdata()
+        self.simulator.go(10)
+
+        favored_nat64_prefix = br.get_br_favored_nat64_prefix()
+        self.assertNotEqual(favored_nat64_prefix, local_nat64_prefix)
+        infra_nat64_prefix = favored_nat64_prefix
 
         self.assertEqual(len(br.get_netdata_nat64_prefix()), 1)
         nat64_prefix = br.get_netdata_nat64_prefix()[0]
@@ -101,7 +120,7 @@
             'Translator': NAT64_STATE_NOT_RUNNING
         })
 
-        # Case 2 Withdraw infrastructure prefix when a smaller prefix in medium
+        # Case 3 Unpublish infrastructure prefix when a smaller prefix in medium
         # preference is present
         br.add_route(SMALL_NAT64_PREFIX, stable=False, nat64=True, prf='med')
         br.register_netdata()
@@ -121,7 +140,7 @@
         self.assertEqual(len(br.get_netdata_nat64_prefix()), 1)
         self.assertEqual(nat64_prefix, infra_nat64_prefix)
 
-        # Case 3 No change when a smaller prefix in low preference is present
+        # Case 4 No change when a smaller prefix in low preference is present
         br.add_route(SMALL_NAT64_PREFIX, stable=False, nat64=True, prf='low')
         br.register_netdata()
         self.simulator.go(5)
@@ -137,7 +156,7 @@
         br.register_netdata()
         self.simulator.go(5)
 
-        # Case 4 Infrastructure nat64 prefix no longer presents
+        # Case 5 Infrastructure nat64 prefix no longer presents
         br.bash("service bind9 stop")
         self.simulator.go(NAT64_PREFIX_REFRESH_DELAY)
 
@@ -150,7 +169,7 @@
             'Translator': NAT64_STATE_ACTIVE
         })
 
-        # Case 5 Infrastructure nat64 prefix is recovered
+        # Case 6 Infrastructure nat64 prefix is recovered
         br.bash("service bind9 start")
         self.simulator.go(NAT64_PREFIX_REFRESH_DELAY)
 
@@ -162,7 +181,7 @@
             'Translator': NAT64_STATE_NOT_RUNNING
         })
 
-        # Case 6 Change infrastructure nat64 prefix
+        # Case 7 Change infrastructure nat64 prefix
         br.bash("sed -i 's/dns64 /\/\/dns64 /' /etc/bind/named.conf.options")
         br.bash("sed -i '/\/\/dns64 /a dns64 " + SMALL_NAT64_PREFIX + " {};' /etc/bind/named.conf.options")
         br.bash("service bind9 restart")
diff --git a/tests/scripts/thread-cert/border_router/test_border_router_as_fed.py b/tests/scripts/thread-cert/border_router/test_border_router_as_fed.py
old mode 100644
new mode 100755
diff --git a/tests/scripts/thread-cert/border_router/test_dnssd_instance_name_with_space.py b/tests/scripts/thread-cert/border_router/test_dnssd_instance_name_with_space.py
old mode 100644
new mode 100755
diff --git a/tests/scripts/thread-cert/border_router/test_dnssd_server.py b/tests/scripts/thread-cert/border_router/test_dnssd_server.py
old mode 100644
new mode 100755
diff --git a/tests/scripts/thread-cert/border_router/test_dnssd_server_multi_border_routers.py b/tests/scripts/thread-cert/border_router/test_dnssd_server_multi_border_routers.py
old mode 100644
new mode 100755
index 4617848..71166ef
--- a/tests/scripts/thread-cert/border_router/test_dnssd_server_multi_border_routers.py
+++ b/tests/scripts/thread-cert/border_router/test_dnssd_server_multi_border_routers.py
@@ -155,6 +155,7 @@
         def is_int(x):
             return isinstance(x, int)
 
+        # 1. Check hosts & services published by Advertising Proxy.
         # check if AAAA query works
         dig_result = host.dns_dig(br2_addr, host1_full_name, 'AAAA')
         self._assert_dig_result_matches(dig_result, {
@@ -219,7 +220,64 @@
             ],
         }])
 
-        # check some invalid queries
+        # 2. Check the host & service published by a WiFi host.
+        # check if AAAA query works
+        wifi_host_linklocal_address = 'fe80::1234'
+        wifi_host_routable_address = '2402::abcd'
+        wifi_host_full_name = f'wifi-host.{DOMAIN}'
+        wifi_service_instance_full_name = f'wifi-service._host._tcp.{DOMAIN}'
+        host.publish_mdns_host('wifi-host', [wifi_host_linklocal_address, wifi_host_routable_address])
+        host.publish_mdns_service('wifi-service', '_host._tcp', 12345, 'wifi-host', {'k1': 'v1', 'k2': 'v2'})
+        dig_result = host.dns_dig(br2_addr, wifi_host_full_name, 'AAAA')
+        self._assert_dig_result_matches(
+            dig_result, {
+                'QUESTION': [(wifi_host_full_name, 'IN', 'AAAA')],
+                'ANSWER': [(wifi_host_full_name, 'IN', 'AAAA', wifi_host_routable_address),],
+            })
+
+        # check if SRV query works
+        dig_result = host.dns_dig(br2_addr, wifi_service_instance_full_name, 'SRV')
+        self._assert_dig_result_matches(
+            dig_result, {
+                'QUESTION': [(wifi_service_instance_full_name, 'IN', 'SRV')],
+                'ANSWER': [(wifi_service_instance_full_name, 'IN', 'SRV', is_int, is_int, 12345, wifi_host_full_name),
+                          ],
+                'ADDITIONAL': [(wifi_host_full_name, 'IN', 'AAAA', wifi_host_routable_address),],
+            })
+
+        # check if TXT query works
+        dig_result = host.dns_dig(br2_addr, wifi_service_instance_full_name, 'TXT')
+        self._assert_dig_result_matches(
+            dig_result, {
+                'QUESTION': [(wifi_service_instance_full_name, 'IN', 'TXT')],
+                'ANSWER': [(wifi_service_instance_full_name, 'IN', 'TXT', {
+                    'k1': 'v1',
+                    'k2': 'v2'
+                })],
+            })
+
+        # check if PTR query works
+        dig_result = host.dns_dig(br2_addr, f'_host._tcp.{DOMAIN}', 'PTR')
+
+        self._assert_dig_result_matches_any(dig_result, [{
+            'QUESTION': [(f'_host._tcp.{DOMAIN}', 'IN', 'PTR')],
+            'ANSWER': [(f'_host._tcp.{DOMAIN}', 'IN', 'PTR', wifi_service_instance_full_name)],
+            'ADDITIONAL': [
+                (wifi_service_instance_full_name, 'IN', 'SRV', is_int, is_int, 12345, wifi_host_full_name),
+                (wifi_service_instance_full_name, 'IN', 'TXT', {
+                    'k1': 'v1',
+                    'k2': 'v2'
+                }),
+                (wifi_host_full_name, 'IN', 'AAAA', wifi_host_routable_address),
+            ],
+        }])
+
+        host.bash('pkill avahi-publish')
+
+        # 3. Verify Discovery Proxy works for _meshcop._udp published by BR.
+        self._verify_discovery_proxy_meshcop(br2_addr, br2.get_network_name(), host)
+
+        # 4. Check some invalid queries
         for qtype in ['A', 'CNAME']:
             dig_result = host.dns_dig(br2_addr, host1_full_name, qtype)
             self._assert_dig_result_matches(dig_result, {
@@ -232,9 +290,6 @@
                 'status': 'NXDOMAIN',
             })
 
-        # verify Discovery Proxy works for _meshcop._udp
-        self._verify_discovery_proxy_meshcop(br2_addr, br2.get_network_name(), host)
-
     def _verify_discovery_proxy_meshcop(self, server_addr, network_name, digger):
         dp_service_name = '_meshcop._udp.default.service.arpa.'
         dp_hostname = lambda x: x.endswith('.default.service.arpa.')
@@ -300,7 +355,8 @@
         client.srp_client_set_host_address(*addrs)
         client.srp_client_add_service(instancename, SERVICE, port, priority, weight)
 
-        self.simulator.go(5)
+        self.simulator.go(10)
+
         self.assertEqual(client.srp_client_get_host_state(), 'Registered')
 
     def _assert_have_question(self, dig_result, question):
diff --git a/tests/scripts/thread-cert/border_router/test_end_device_udp_reachability.py b/tests/scripts/thread-cert/border_router/test_end_device_udp_reachability.py
old mode 100644
new mode 100755
diff --git a/tests/scripts/thread-cert/border_router/test_external_route.py b/tests/scripts/thread-cert/border_router/test_external_route.py
old mode 100644
new mode 100755
diff --git a/tests/scripts/thread-cert/border_router/test_firewall.py b/tests/scripts/thread-cert/border_router/test_firewall.py
old mode 100644
new mode 100755
diff --git a/tests/scripts/thread-cert/border_router/test_manual_address.py b/tests/scripts/thread-cert/border_router/test_manual_address.py
old mode 100644
new mode 100755
diff --git a/tests/scripts/thread-cert/border_router/test_manual_maddress.py b/tests/scripts/thread-cert/border_router/test_manual_maddress.py
old mode 100644
new mode 100755
index 1bbe190..536d3ad
--- a/tests/scripts/thread-cert/border_router/test_manual_maddress.py
+++ b/tests/scripts/thread-cert/border_router/test_manual_maddress.py
@@ -108,8 +108,7 @@
         # packet back to Host.
         # TD receives the MPL packet containing an encapsulated ping packet to
         # MA1, sent by Host, and unicasts a ping response packet back to Host.
-        pkts.filter_eth_src(vars['TD_ETH']) \
-            .filter_ipv6_dst(_pkt.ipv6.src) \
+        pkts.filter_ipv6_dst(_pkt.ipv6.src) \
             .filter_ping_reply(identifier=_pkt.icmpv6.echo.identifier) \
             .must_next()
 
diff --git a/tests/scripts/thread-cert/border_router/test_manual_omr_prefix.py b/tests/scripts/thread-cert/border_router/test_manual_omr_prefix.py
old mode 100644
new mode 100755
diff --git a/tests/scripts/thread-cert/border_router/test_multi_border_routers.py b/tests/scripts/thread-cert/border_router/test_multi_border_routers.py
index 2e5855b..ab354d1 100755
--- a/tests/scripts/thread-cert/border_router/test_multi_border_routers.py
+++ b/tests/scripts/thread-cert/border_router/test_multi_border_routers.py
@@ -147,8 +147,6 @@
         self.assertEqual(len(router2.get_netdata_non_nat64_prefixes()), 2)
 
         br1_on_link_prefix = br1.get_br_on_link_prefix()
-        self.assertEqual(br1_on_link_prefix, br1.get_netdata_non_nat64_prefixes()[0])
-        self.assertEqual(br1_on_link_prefix, br1.get_netdata_non_nat64_prefixes()[0])
 
         self.assertEqual(len(br1.get_ip6_address(config.ADDRESS_TYPE.OMR)), 1)
         self.assertEqual(len(router1.get_ip6_address(config.ADDRESS_TYPE.OMR)), 1)
@@ -200,8 +198,6 @@
         self.assertEqual(len(router2.get_netdata_non_nat64_prefixes()), 1)
 
         br2_on_link_prefix = br2.get_br_on_link_prefix()
-        self.assertEqual(set(map(IPv6Network, br2.get_netdata_non_nat64_prefixes())),
-                         set(map(IPv6Network, [br1_on_link_prefix, br2_on_link_prefix])))
 
         self.assertEqual(len(br1.get_ip6_address(config.ADDRESS_TYPE.OMR)), 1)
         self.assertEqual(len(router1.get_ip6_address(config.ADDRESS_TYPE.OMR)), 1)
diff --git a/tests/scripts/thread-cert/border_router/test_multi_thread_networks.py b/tests/scripts/thread-cert/border_router/test_multi_thread_networks.py
index d7ebc80..4e0823b 100755
--- a/tests/scripts/thread-cert/border_router/test_multi_thread_networks.py
+++ b/tests/scripts/thread-cert/border_router/test_multi_thread_networks.py
@@ -33,7 +33,7 @@
 import thread_cert
 
 # Test description:
-#   This test verifies bi-directional connectivity accross multiple Thread networks.
+#   This test verifies bi-directional connectivity across multiple Thread networks.
 #
 # Topology:
 #    -------------(eth)----------------
@@ -130,10 +130,10 @@
 
         # Each BR should independently register an external route for the on-link prefix
         # and OMR prefix in another Thread Network.
-        self.assertTrue(len(br1.get_netdata_non_nat64_prefixes()) == 2)
-        self.assertTrue(len(router1.get_netdata_non_nat64_prefixes()) == 2)
-        self.assertTrue(len(br2.get_netdata_non_nat64_prefixes()) == 2)
-        self.assertTrue(len(router2.get_netdata_non_nat64_prefixes()) == 2)
+        self.assertTrue(len(br1.get_netdata_non_nat64_prefixes()) == 1)
+        self.assertTrue(len(router1.get_netdata_non_nat64_prefixes()) == 1)
+        self.assertTrue(len(br2.get_netdata_non_nat64_prefixes()) == 1)
+        self.assertTrue(len(router2.get_netdata_non_nat64_prefixes()) == 1)
 
         br1_external_routes = br1.get_routes()
         br2_external_routes = br2.get_routes()
diff --git a/tests/scripts/thread-cert/border_router/test_on_link_prefix.py b/tests/scripts/thread-cert/border_router/test_on_link_prefix.py
index 4ff831a..4067236 100755
--- a/tests/scripts/thread-cert/border_router/test_on_link_prefix.py
+++ b/tests/scripts/thread-cert/border_router/test_on_link_prefix.py
@@ -127,10 +127,8 @@
         logging.info("HOST    addrs: %r", host.get_addrs())
 
         self.assertEqual(len(br1.get_netdata_non_nat64_prefixes()), 1)
-        on_link_prefix = br1.get_netdata_non_nat64_prefixes()[0]
-        self.assertEqual(IPv6Network(on_link_prefix), IPv6Network(ON_LINK_PREFIX))
 
-        host_on_link_addr = host.get_matched_ula_addresses(on_link_prefix)[0]
+        host_on_link_addr = host.get_matched_ula_addresses(ON_LINK_PREFIX)[0]
         self.assertTrue(router1.ping(host_on_link_addr))
         self.assertTrue(
             host.ping(router1.get_ip6_address(config.ADDRESS_TYPE.OMR)[0], backbone=True, interface=host_on_link_addr))
@@ -164,18 +162,16 @@
         br2_omr_prefix = br2.get_br_omr_prefix()
         self.assertNotEqual(br1_omr_prefix, br2_omr_prefix)
 
-        # Verify that the Border Routers starts advertsing new on-link prefix
+        # Verify that the Border Routers starts advertising new on-link prefix
         # but don't remove the external routes for the radvd on-link prefix
         # immediately, because the SLAAC addresses are still valid.
-        self.assertEqual(len(br1.get_netdata_non_nat64_prefixes()), 3)
-        self.assertEqual(len(router1.get_netdata_non_nat64_prefixes()), 3)
-        self.assertEqual(len(br2.get_netdata_non_nat64_prefixes()), 2)
-        self.assertEqual(len(router2.get_netdata_non_nat64_prefixes()), 2)
 
-        on_link_prefixes = list(
-            set(br1.get_netdata_non_nat64_prefixes()).intersection(br2.get_netdata_non_nat64_prefixes()))
-        self.assertEqual(len(on_link_prefixes), 1)
-        self.assertEqual(IPv6Network(on_link_prefixes[0]), IPv6Network(br2.get_br_on_link_prefix()))
+        self.assertEqual(len(br1.get_netdata_non_nat64_prefixes()), 1)
+        self.assertEqual(len(router1.get_netdata_non_nat64_prefixes()), 1)
+        self.assertEqual(len(br2.get_netdata_non_nat64_prefixes()), 1)
+        self.assertEqual(len(router2.get_netdata_non_nat64_prefixes()), 1)
+
+        br2_on_link_prefix = br2.get_br_on_link_prefix()
 
         router1_omr_addr = router1.get_ip6_address(config.ADDRESS_TYPE.OMR)[0]
         router2_omr_addr = router2.get_ip6_address(config.ADDRESS_TYPE.OMR)[0]
@@ -184,7 +180,7 @@
         # and preferred Border Router on-link prefix can be reached by Thread
         # devices in network of Border Router 1.
         for host_on_link_addr in [
-                host.get_matched_ula_addresses(on_link_prefixes[0])[0],
+                host.get_matched_ula_addresses(br2_on_link_prefix)[0],
                 host.get_matched_ula_addresses(ON_LINK_PREFIX)[0]
         ]:
             self.assertTrue(router1.ping(host_on_link_addr))
@@ -192,11 +188,6 @@
 
         host_on_link_addr = host.get_matched_ula_addresses(ON_LINK_PREFIX)[0]
 
-        # Make sure that addresses of the deprecated radvd `ON_LINK_PREFIX`
-        # can't be reached by Thread devices in network of Border Router 2.
-        self.assertFalse(router2.ping(host_on_link_addr))
-        self.assertFalse(host.ping(router2_omr_addr, backbone=True, interface=host_on_link_addr))
-
         # Wait 30 seconds for the radvd `ON_LINK_PREFIX` to be invalidated
         # and make sure that Thread devices in both networks can't reach
         # the on-link address.
diff --git a/tests/scripts/thread-cert/border_router/test_plat_udp_accessiblity.py b/tests/scripts/thread-cert/border_router/test_plat_udp_accessiblity.py
old mode 100644
new mode 100755
diff --git a/tests/scripts/thread-cert/border_router/test_single_border_router.py b/tests/scripts/thread-cert/border_router/test_single_border_router.py
index 1028629..a660345 100755
--- a/tests/scripts/thread-cert/border_router/test_single_border_router.py
+++ b/tests/scripts/thread-cert/border_router/test_single_border_router.py
@@ -177,8 +177,6 @@
         # The same local OMR and on-link prefix should be re-register.
         self.assertEqual(br.get_netdata_omr_prefixes(), [omr_prefix])
         self.assertEqual(router.get_netdata_omr_prefixes(), [omr_prefix])
-        self.assertEqual(br.get_netdata_non_nat64_prefixes(), [on_link_prefix])
-        self.assertEqual(router.get_netdata_non_nat64_prefixes(), [on_link_prefix])
 
         self.assertEqual(len(br.get_ip6_address(config.ADDRESS_TYPE.OMR)), 1)
         self.assertEqual(len(router.get_ip6_address(config.ADDRESS_TYPE.OMR)), 1)
@@ -231,8 +229,6 @@
         # The same local OMR and on-link prefix should be re-registered.
         self.assertEqual(br.get_netdata_omr_prefixes(), [omr_prefix])
         self.assertEqual(router.get_netdata_omr_prefixes(), [omr_prefix])
-        self.assertEqual(br.get_netdata_non_nat64_prefixes(), [on_link_prefix])
-        self.assertEqual(router.get_netdata_non_nat64_prefixes(), [on_link_prefix])
 
         self.assertEqual(len(br.get_ip6_address(config.ADDRESS_TYPE.OMR)), 1)
         self.assertEqual(len(router.get_ip6_address(config.ADDRESS_TYPE.OMR)), 1)
@@ -285,8 +281,6 @@
         # The same local OMR and on-link prefix should be re-registered.
         self.assertEqual(br.get_netdata_omr_prefixes(), [omr_prefix])
         self.assertEqual(router.get_netdata_omr_prefixes(), [omr_prefix])
-        self.assertEqual(br.get_netdata_non_nat64_prefixes(), [on_link_prefix])
-        self.assertEqual(router.get_netdata_non_nat64_prefixes(), [on_link_prefix])
 
         self.assertEqual(len(br.get_ip6_address(config.ADDRESS_TYPE.OMR)), 1)
         self.assertEqual(len(router.get_ip6_address(config.ADDRESS_TYPE.OMR)), 1)
@@ -320,8 +314,8 @@
         br.start_radvd_service(prefix=config.ONLINK_GUA_PREFIX, slaac=True)
         self.simulator.go(5)
 
-        self.assertEqual(len(br.get_netdata_non_nat64_prefixes()), 2)
-        self.assertEqual(len(router.get_netdata_non_nat64_prefixes()), 2)
+        self.assertEqual(len(br.get_netdata_non_nat64_prefixes()), 1)
+        self.assertEqual(len(router.get_netdata_non_nat64_prefixes()), 1)
 
         self.assertTrue(router.ping(host.get_ip6_address(config.ADDRESS_TYPE.ONLINK_GUA)[0]))
         self.assertTrue(router.ping(host.get_ip6_address(config.ADDRESS_TYPE.ONLINK_ULA)[0]))
diff --git a/tests/scripts/thread-cert/border_router/test_srp_register_500_services_br.py b/tests/scripts/thread-cert/border_router/test_srp_register_500_services_br.py
old mode 100644
new mode 100755
diff --git a/tests/scripts/thread-cert/border_router/test_trel_connectivity.py b/tests/scripts/thread-cert/border_router/test_trel_connectivity.py
old mode 100644
new mode 100755
diff --git a/tests/scripts/thread-cert/coap.py b/tests/scripts/thread-cert/coap.py
old mode 100755
new mode 100644
diff --git a/tests/scripts/thread-cert/command.py b/tests/scripts/thread-cert/command.py
old mode 100755
new mode 100644
index e194493..419840f
--- a/tests/scripts/thread-cert/command.py
+++ b/tests/scripts/thread-cert/command.py
@@ -682,7 +682,7 @@
 
 
 def check_payload_same(tp1, tp2):
-    """Verfiy two payloads are totally the same.
+    """Verify two payloads are totally the same.
        A payload is a tuple of tlvs.
     """
     assert len(tp1) == len(tp2)
diff --git a/tests/scripts/thread-cert/common.py b/tests/scripts/thread-cert/common.py
old mode 100755
new mode 100644
diff --git a/tests/scripts/thread-cert/config.py b/tests/scripts/thread-cert/config.py
old mode 100755
new mode 100644
index cf04cbe..e378ec1
--- a/tests/scripts/thread-cert/config.py
+++ b/tests/scripts/thread-cert/config.py
@@ -330,7 +330,7 @@
         network_layer.TlvType.XTAL_ACCURACY:
             network_layer.XtalAccuracyFactory(),
         # Routing information are distributed in a Thread network by MLE Routing TLV
-        # which is in fact MLE Route64 TLV. Thread specificaton v1.1. - Chapter 5.20
+        # which is in fact MLE Route64 TLV. Thread specification v1.1. - Chapter 5.20
         network_layer.TlvType.MLE_ROUTING:
             create_default_mle_tlv_route64_factory(),
         network_layer.TlvType.IPv6_ADDRESSES:
diff --git a/tests/scripts/thread-cert/debug.py b/tests/scripts/thread-cert/debug.py
old mode 100755
new mode 100644
diff --git a/tests/scripts/thread-cert/dtls.py b/tests/scripts/thread-cert/dtls.py
old mode 100755
new mode 100644
diff --git a/tests/scripts/thread-cert/ipv6.py b/tests/scripts/thread-cert/ipv6.py
old mode 100755
new mode 100644
index ab00a26..f491574
--- a/tests/scripts/thread-cert/ipv6.py
+++ b/tests/scripts/thread-cert/ipv6.py
@@ -622,7 +622,7 @@
     | Next Header | Reserved | Fragment Offset | Res | M | Identification |
     +-------------+----------+-----------------+-----+---+----------------+
 
-    Fragment extention header consists of:
+    Fragment extension header consists of:
         - next_header type (8 bit)
         - fragment offset which is multiple of 8 (13 bit)
         - more_flag to indicate further data (1 bit)
@@ -1010,7 +1010,7 @@
         hdr_ext_len = ord(data.read(1))
 
         # Note! Two bytes were read (next_header and hdr_ext_len) so they must
-        # be substracted from header length
+        # be subtracted from header length
         hop_by_hop_length = (self._calculate_extension_header_length(hdr_ext_len) - 2)
 
         hop_by_hop_data = data.read(hop_by_hop_length)
diff --git a/tests/scripts/thread-cert/lowpan.py b/tests/scripts/thread-cert/lowpan.py
old mode 100755
new mode 100644
diff --git a/tests/scripts/thread-cert/mac802154.py b/tests/scripts/thread-cert/mac802154.py
old mode 100755
new mode 100644
diff --git a/tests/scripts/thread-cert/mcast6.py b/tests/scripts/thread-cert/mcast6.py
index b090fd9..8699d5c 100755
--- a/tests/scripts/thread-cert/mcast6.py
+++ b/tests/scripts/thread-cert/mcast6.py
@@ -57,7 +57,7 @@
     ifname = ctypes.create_string_buffer(32)
     ifname = libc.if_indextoname(index, ifname)
     if not ifname:
-        raise RuntimeError("Inavlid Index")
+        raise RuntimeError("Invalid Index")
     return ifname
 
 
diff --git a/tests/scripts/thread-cert/mesh_cop.py b/tests/scripts/thread-cert/mesh_cop.py
old mode 100755
new mode 100644
diff --git a/tests/scripts/thread-cert/message.py b/tests/scripts/thread-cert/message.py
old mode 100755
new mode 100644
diff --git a/tests/scripts/thread-cert/mle.py b/tests/scripts/thread-cert/mle.py
old mode 100755
new mode 100644
index 0c826ad..2bf6c93
--- a/tests/scripts/thread-cert/mle.py
+++ b/tests/scripts/thread-cert/mle.py
@@ -332,7 +332,7 @@
         return (self.output == other.output and self.input == other.input and self.route == other.route)
 
     def __repr__(self):
-        return "LinkQualityAndRouteData(ouput={}, input={}, route={})".format(self.output, self.input, self.route)
+        return "LinkQualityAndRouteData(output={}, input={}, route={})".format(self.output, self.input, self.route)
 
 
 class LinkQualityAndRouteDataFactory:
diff --git a/tests/scripts/thread-cert/net_crypto.py b/tests/scripts/thread-cert/net_crypto.py
old mode 100755
new mode 100644
diff --git a/tests/scripts/thread-cert/network_data.py b/tests/scripts/thread-cert/network_data.py
old mode 100755
new mode 100644
diff --git a/tests/scripts/thread-cert/network_diag.py b/tests/scripts/thread-cert/network_diag.py
old mode 100755
new mode 100644
diff --git a/tests/scripts/thread-cert/network_layer.py b/tests/scripts/thread-cert/network_layer.py
old mode 100755
new mode 100644
diff --git a/tests/scripts/thread-cert/node.py b/tests/scripts/thread-cert/node.py
index 96a626e..674cf6d 100755
--- a/tests/scripts/thread-cert/node.py
+++ b/tests/scripts/thread-cert/node.py
@@ -171,12 +171,10 @@
         self.bash('service otbr-agent stop')
 
     def stop_mdns_service(self):
-        self.bash('service avahi-daemon stop')
-        self.bash('service mdns stop')
+        self.bash('service avahi-daemon stop; service mdns stop; !(cat /proc/net/udp | grep -i :14E9)')
 
     def start_mdns_service(self):
-        self.bash('service avahi-daemon start')
-        self.bash('service mdns start')
+        self.bash('service avahi-daemon start; service mdns start; cat /proc/net/udp | grep -i :14E9')
 
     def start_ot_ctl(self):
         cmd = f'docker exec -i {self._docker_name} ot-ctl'
@@ -2382,6 +2380,10 @@
         self.send_command(f'netdata publish route {prefix} {flags} {prf}')
         self._expect_done()
 
+    def netdata_publish_replace(self, old_prefix, prefix, flags='s', prf='med'):
+        self.send_command(f'netdata publish replace {old_prefix} {prefix} {flags} {prf}')
+        self._expect_done()
+
     def netdata_unpublish_prefix(self, prefix):
         self.send_command(f'netdata unpublish {prefix}')
         self._expect_done()
@@ -3171,7 +3173,7 @@
     def _parse_linkmetrics_query_result(self, lines):
         """Parse link metrics query result"""
 
-        # Exmaple of command output:
+        # Example of command output:
         # ['Received Link Metrics Report from: fe80:0:0:0:146e:a00:0:1',
         #  '- PDU Counter: 1 (Count/Summation)',
         #  '- LQI: 0 (Exponential Moving Average)',
@@ -3642,6 +3644,30 @@
                   (self.ETH_DEV, self.ETH_DEV))
         self.bash(f'ip -6 neigh list dev {self.ETH_DEV}')
 
+    def publish_mdns_service(self, instance_name, service_type, port, host_name, txt):
+        """Publish an mDNS service on the Ethernet.
+
+        :param instance_name: the service instance name.
+        :param service_type: the service type in format of '<service_type>.<protocol>'.
+        :param port: the port the service is at.
+        :param host_name: the host name this service points to. The domain
+                          should not be included.
+        :param txt: a dictionary containing the key-value pairs of the TXT record.
+        """
+        txt_string = ' '.join([f'{key}={value}' for key, value in txt.items()])
+        self.bash(f'avahi-publish -s {instance_name}  {service_type} {port} -H {host_name}.local {txt_string} &')
+
+    def publish_mdns_host(self, hostname, addresses):
+        """Publish an mDNS host on the Ethernet
+
+        :param host_name: the host name this service points to. The domain
+                          should not be included.
+        :param addresses: a list of strings representing the addresses to
+                          be registered with the host.
+        """
+        for address in addresses:
+            self.bash(f'avahi-publish -a {hostname}.local {address} &')
+
     def browse_mdns_services(self, name, timeout=2):
         """ Browse mDNS services on the ethernet.
 
diff --git a/tests/scripts/thread-cert/pcap.py b/tests/scripts/thread-cert/pcap.py
old mode 100755
new mode 100644
diff --git a/tests/scripts/thread-cert/pktverify/utils.py b/tests/scripts/thread-cert/pktverify/utils.py
index 7d57ce0..9eeb3ed 100644
--- a/tests/scripts/thread-cert/pktverify/utils.py
+++ b/tests/scripts/thread-cert/pktverify/utils.py
@@ -140,11 +140,11 @@
 
 
 def colon_hex(hexstr, interval) -> str:
-    """ Convert hexstr to colon seperated string every interval
+    """ Convert hexstr to colon separated string every interval
 
     :param hexstr: The hex string to convert.
     :param interval: The interval number.
-    :return: The colon seperated string.
+    :return: The colon separated string.
     """
     assert len(hexstr) % interval == 0
     return ':'.join(hexstr[i:i + interval] for i in range(0, len(hexstr), interval))
diff --git a/tests/scripts/thread-cert/run_cert_suite.py b/tests/scripts/thread-cert/run_cert_suite.py
index 6af09d8..09fc19d 100755
--- a/tests/scripts/thread-cert/run_cert_suite.py
+++ b/tests/scripts/thread-cert/run_cert_suite.py
@@ -56,21 +56,28 @@
     subprocess.run(cmd, shell=True, check=check, stdout=stdout)
 
 
-def run_cert(job_id: int, port_offset: int, script: str):
+def run_cert(job_id: int, port_offset: int, script: str, run_directory: str):
+    if not os.access(script, os.X_OK):
+        logging.warning('Skip test %s, not executable', script)
+        return
+
     try:
         test_name = os.path.splitext(os.path.basename(script))[0] + '_' + str(job_id)
-        logfile = f'{test_name}.log'
+        logfile = f'{run_directory}/{test_name}.log' if run_directory else f'{test_name}.log'
         env = os.environ.copy()
         env['PORT_OFFSET'] = str(port_offset)
         env['TEST_NAME'] = test_name
+        env['PYTHONPATH'] = os.path.dirname(os.path.abspath(__file__))
 
         try:
             print(f'Running {test_name}')
             with open(logfile, 'wt') as output:
-                subprocess.check_call(["python3", script],
+                abs_script = os.path.abspath(script)
+                subprocess.check_call(abs_script,
                                       stdout=output,
                                       stderr=output,
                                       stdin=subprocess.DEVNULL,
+                                      cwd=run_directory,
                                       env=env)
         except subprocess.CalledProcessError:
             bash(f'cat {logfile} 1>&2')
@@ -107,10 +114,12 @@
     import argparse
     parser = argparse.ArgumentParser(description='Process some integers.')
     parser.add_argument('--multiply', type=int, default=1, help='run each test for multiple times')
+    parser.add_argument('--run-directory', type=str, default=None, help='run each test in the specified directory')
     parser.add_argument("scripts", nargs='+', type=str, help='specify Backbone test scripts')
 
     args = parser.parse_args()
     logging.info("Max jobs: %d", MAX_JOBS)
+    logging.info("Run directory: %s", args.run_directory or '.')
     logging.info("Multiply: %d", args.multiply)
     logging.info("Test scripts: %d", len(args.scripts))
     return args
@@ -141,7 +150,7 @@
         self._pool.put_nowait(port_offset)
 
 
-def run_tests(scripts: List[str], multiply: int = 1):
+def run_tests(scripts: List[str], multiply: int = 1, run_directory: str = None):
     script_fail_count = Counter()
     script_succ_count = Counter()
 
@@ -168,7 +177,7 @@
     for script, i in script_ids:
         port_offset = port_offset_pool.allocate()
         pool.apply_async(
-            run_cert, [i, port_offset, script],
+            run_cert, [i, port_offset, script, run_directory],
             callback=lambda ret, port_offset=port_offset, script=script: pass_callback(port_offset, script),
             error_callback=lambda err, port_offset=port_offset, script=script: error_callback(
                 port_offset, script, err))
@@ -189,7 +198,7 @@
         setup_backbone_env()
 
     try:
-        fail_count = run_tests(args.scripts, args.multiply)
+        fail_count = run_tests(args.scripts, args.multiply, args.run_directory)
         exit(fail_count)
     finally:
         if has_backbone_tests:
diff --git a/tests/scripts/thread-cert/sniffer_transport.py b/tests/scripts/thread-cert/sniffer_transport.py
old mode 100755
new mode 100644
diff --git a/tests/scripts/thread-cert/test_dnssd_name_with_special_chars.py b/tests/scripts/thread-cert/test_dnssd_name_with_special_chars.py
old mode 100644
new mode 100755
diff --git a/tests/scripts/thread-cert/test_netdata_publisher.py b/tests/scripts/thread-cert/test_netdata_publisher.py
index 5485440..f1a1c5d 100755
--- a/tests/scripts/thread-cert/test_netdata_publisher.py
+++ b/tests/scripts/thread-cert/test_netdata_publisher.py
@@ -461,6 +461,19 @@
         routes = leader.get_routes()
         self.check_num_of_routes(routes, num - 1, 0, 1)
 
+        # Replace the published route on leader with '::/0'.
+        leader.netdata_publish_replace(EXTERNAL_ROUTE, '::/0', EXTERNAL_FLAGS, 'med')
+        self.simulator.go(0.2)
+        routes = leader.get_routes()
+        self.assertEqual([route.split(' ')[0] == '::/0' for route in routes].count(True), 1)
+        self.check_num_of_routes(routes, num - 1, 1, 0)
+
+        # Replace it back to the original route.
+        leader.netdata_publish_replace('::/0', EXTERNAL_ROUTE, EXTERNAL_FLAGS, 'high')
+        self.simulator.go(WAIT_TIME)
+        routes = leader.get_routes()
+        self.check_num_of_routes(routes, num - 1, 0, 1)
+
         # Publish the same prefix on leader as an on-mesh prefix. Make
         # sure it is removed from external routes and now seen in the
         # prefix list.
diff --git a/tests/scripts/thread-cert/test_ping_lla_src.py b/tests/scripts/thread-cert/test_ping_lla_src.py
old mode 100644
new mode 100755
diff --git a/tests/scripts/thread-cert/thread_cert.py b/tests/scripts/thread-cert/thread_cert.py
old mode 100755
new mode 100644
diff --git a/tests/scripts/thread-cert/tlvs_parsing.py b/tests/scripts/thread-cert/tlvs_parsing.py
old mode 100755
new mode 100644
diff --git a/tests/scripts/thread-cert/v1_2_test_domain_unicast_address.py b/tests/scripts/thread-cert/v1_2_test_domain_unicast_address.py
index 13c1950..7a98680 100755
--- a/tests/scripts/thread-cert/v1_2_test_domain_unicast_address.py
+++ b/tests/scripts/thread-cert/v1_2_test_domain_unicast_address.py
@@ -137,7 +137,7 @@
 
     def __check_dua_registration(self, node, iid, dp_cid):
         ''' Check whether or not the specified Domain Unicast Address is registered in Address
-        Registraion TLV.
+        Registration TLV.
 
         Args:
             node (int) : The device id
diff --git a/tests/scripts/thread-cert/v1_2_test_domain_unicast_address_registration.py b/tests/scripts/thread-cert/v1_2_test_domain_unicast_address_registration.py
index dfbf314..4053fec 100755
--- a/tests/scripts/thread-cert/v1_2_test_domain_unicast_address_registration.py
+++ b/tests/scripts/thread-cert/v1_2_test_domain_unicast_address_registration.py
@@ -165,13 +165,13 @@
         '''
         return ''.join(ipaddress.ip_address(address).exploded.split(':')[4:])
 
-    def __check_dua_registration_tmf(self, node, occurences=1, ml_eid=None):
+    def __check_dua_registration_tmf(self, node, occurrences=1, ml_eid=None):
 
         messages = self.simulator.get_messages_sent_by(node)
-        for i in range(occurences):
+        for i in range(occurrences):
             msg = messages.next_coap_message('0.02', '/n/dr', False)
             assert msg, 'Expected {}, but {}th not found\n node: {}(extaddr: {})'.format(
-                occurences, i + 1, node, self.nodes[node].get_addr64())
+                occurrences, i + 1, node, self.nodes[node].get_addr64())
             if ml_eid:
                 ml_eid_tlv = msg.get_coap_message_tlv(network_layer.MlEid)
                 self.assertEqual(ml_eid, ml_eid_tlv.ml_eid.hex())
diff --git a/tests/scripts/thread-cert/v1_2_test_multicast_listener_registration.py b/tests/scripts/thread-cert/v1_2_test_multicast_listener_registration.py
index c6d369e..1887536 100755
--- a/tests/scripts/thread-cert/v1_2_test_multicast_listener_registration.py
+++ b/tests/scripts/thread-cert/v1_2_test_multicast_listener_registration.py
@@ -891,7 +891,7 @@
         self.simulator.go(WAIT_REDUNDANCE)
         self.__check_send_mlr_req(parent_id, MA1, should_send=True, expect_mlr_rsp=True)
 
-        # Parent should not register MA1 of Child 1 because it's already registerd
+        # Parent should not register MA1 of Child 1 because it's already registered
         self.flush_all()
         self.nodes[meds[0]].add_ipmaddr(MA1)
         self.simulator.go(PARENT_AGGREGATE_DELAY + WAIT_REDUNDANCE)
diff --git a/tests/scripts/thread-cert/v1_2_test_multicast_registration.py b/tests/scripts/thread-cert/v1_2_test_multicast_registration.py
index 67537b7..8001261 100755
--- a/tests/scripts/thread-cert/v1_2_test_multicast_registration.py
+++ b/tests/scripts/thread-cert/v1_2_test_multicast_registration.py
@@ -144,7 +144,7 @@
                                        in_address_registration=True):
         ''' Check whether or not the addition of the multicast address on the specific node
         would trigger Child Update Request for multicast address registration via Address
-        Registraion TLV.
+        Registration TLV.
 
         Args:
             node (int) : The device id
diff --git a/tests/scripts/thread-cert/wpan.py b/tests/scripts/thread-cert/wpan.py
old mode 100755
new mode 100644
diff --git a/tests/toranj/cli/cli.py b/tests/toranj/cli/cli.py
index b8a8b02..fd24205 100644
--- a/tests/toranj/cli/cli.py
+++ b/tests/toranj/cli/cli.py
@@ -214,6 +214,9 @@
     def get_state(self):
         return self._cli_single_output('state', expected_outputs=['detached', 'child', 'router', 'leader', 'disabled'])
 
+    def get_version(self):
+        return self._cli_single_output('version')
+
     def get_channel(self):
         return self._cli_single_output('channel')
 
@@ -357,6 +360,24 @@
     def get_eidcache(self):
         return self.cli('eidcache')
 
+    def get_vendor_name(self):
+        return self._cli_single_output('vendor name')
+
+    def set_vendor_name(self, name):
+        self._cli_no_output('vendor name', name)
+
+    def get_vendor_model(self):
+        return self._cli_single_output('vendor model')
+
+    def set_vendor_model(self, model):
+        self._cli_no_output('vendor model', model)
+
+    def get_vendor_sw_version(self):
+        return self._cli_single_output('vendor swversion')
+
+    def set_vendor_sw_version(self, version):
+        return self._cli_no_output('vendor swversion', version)
+
     #- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
     # netdata
 
diff --git a/tests/toranj/cli/test-012-reset-recovery.py b/tests/toranj/cli/test-012-reset-recovery.py
index 8af8514..ea92a43 100755
--- a/tests/toranj/cli/test-012-reset-recovery.py
+++ b/tests/toranj/cli/test-012-reset-recovery.py
@@ -55,7 +55,7 @@
 # Form topology
 
 leader.form('reset')
-child1.join(leader, cli.JOIN_TYPE_END_DEVICE)
+child1.join(leader, cli.JOIN_TYPE_REED)
 child2.join(leader, cli.JOIN_TYPE_END_DEVICE)
 
 verify(leader.get_state() == 'leader')
@@ -135,6 +135,24 @@
 
 verify_within(check_leader_neighbor_table, 10)
 
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+# Reset `child` and make sure it re-attaches successfully.
+
+del child1
+child1 = cli.Node(index=3)
+child1.set_router_eligible('disable')
+child1.interface_up()
+child1.thread_start()
+
+
+def check_child1_state():
+    verify(child1.get_state() == 'child')
+    table = child1.get_router_table()
+    verify(len(table) == 2)
+
+
+verify_within(check_child1_state, 10)
+
 # -----------------------------------------------------------------------------------------------------------------------
 # Test finished
 
diff --git a/tests/toranj/cli/test-018-next-hop-and-path-cost.py b/tests/toranj/cli/test-018-next-hop-and-path-cost.py
index b31b2f8..ebaca1f 100755
--- a/tests/toranj/cli/test-018-next-hop-and-path-cost.py
+++ b/tests/toranj/cli/test-018-next-hop-and-path-cost.py
@@ -120,7 +120,7 @@
 
 
 def parse_nexthop(line):
-    # Exmaple: "0x5000 cost:3" -> (0x5000, 3).
+    # Example: "0x5000 cost:3" -> (0x5000, 3).
     items = line.strip().split(' ', 2)
     return (int(items[0], 16), int(items[1].split(':')[1]))
 
diff --git a/tests/toranj/cli/test-020-net-diag-vendor-info.py b/tests/toranj/cli/test-020-net-diag-vendor-info.py
new file mode 100755
index 0000000..6f205cd
--- /dev/null
+++ b/tests/toranj/cli/test-020-net-diag-vendor-info.py
@@ -0,0 +1,184 @@
+#!/usr/bin/env python3
+#
+#  Copyright (c) 2023, The OpenThread Authors.
+#  All rights reserved.
+#
+#  Redistribution and use in source and binary forms, with or without
+#  modification, are permitted provided that the following conditions are met:
+#  1. Redistributions of source code must retain the above copyright
+#     notice, this list of conditions and the following disclaimer.
+#  2. Redistributions in binary form must reproduce the above copyright
+#     notice, this list of conditions and the following disclaimer in the
+#     documentation and/or other materials provided with the distribution.
+#  3. Neither the name of the copyright holder nor the
+#     names of its contributors may be used to endorse or promote products
+#     derived from this software without specific prior written permission.
+#
+#  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+#  AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+#  IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+#  ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+#  LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+#  CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+#  SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+#  INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+#  CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+#  ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+#  POSSIBILITY OF SUCH DAMAGE.
+
+from cli import verify
+from cli import verify_within
+import cli
+import time
+
+# -----------------------------------------------------------------------------------------------------------------------
+# Test description: Network Diagnostics Vendor Name, Vendor Model, Vendor SW Version TLVs.
+#
+# Network topology
+#
+#      r1 ---- r2
+#
+
+test_name = __file__[:-3] if __file__.endswith('.py') else __file__
+print('-' * 120)
+print('Starting \'{}\''.format(test_name))
+
+# -----------------------------------------------------------------------------------------------------------------------
+# Creating `cli.Node` instances
+
+speedup = 40
+cli.Node.set_time_speedup_factor(speedup)
+
+r1 = cli.Node()
+r2 = cli.Node()
+
+# -----------------------------------------------------------------------------------------------------------------------
+# Form topology
+
+r1.form('netdiag-vendor')
+r2.join(r1)
+
+verify(r1.get_state() == 'leader')
+verify(r2.get_state() == 'router')
+
+# -----------------------------------------------------------------------------------------------------------------------
+# Test Implementation
+
+VENDOR_NAME_TLV = 25
+VENDOR_MODEL_TLV = 26
+VENDOR_SW_VERSION_TLV = 27
+THREAD_STACK_VERSION_TLV = 28
+
+#- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+# Check setting vendor name, model, ans sw version
+
+r1.set_vendor_name('nest')
+r1.set_vendor_model('marble')
+r1.set_vendor_sw_version('ot-1.3.1')
+
+verify(r1.get_vendor_name() == 'nest')
+verify(r1.get_vendor_model() == 'marble')
+verify(r1.get_vendor_sw_version() == 'ot-1.3.1')
+
+#- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+# Check invalid names (too long)
+
+# Vendor name should accept up to 32 chars
+
+r2.set_vendor_name('01234567890123456789012345678901')  # 32 chars
+
+errored = False
+
+try:
+    r2.set_vendor_name('012345678901234567890123456789012')  # 33 chars
+except cli.CliError as e:
+    verify(e.message == 'InvalidArgs')
+    errored = True
+
+verify(errored)
+
+# Vendor model should accept up to 32 chars
+
+r2.set_vendor_model('01234567890123456789012345678901')  # 32 chars
+
+errored = False
+
+try:
+    r2.set_vendor_model('012345678901234567890123456789012')  # 33 chars
+except cli.CliError as e:
+    verify(e.message == 'InvalidArgs')
+    errored = True
+
+verify(errored)
+
+# Vendor SW version should accept up to 16 chars
+
+r2.set_vendor_sw_version('0123456789012345')  # 16 chars
+
+errored = False
+
+try:
+    r2.set_vendor_sw_version('01234567890123456')  # 17 chars
+except cli.CliError as e:
+    verify(e.message == 'InvalidArgs')
+    errored = True
+
+verify(errored)
+
+#- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+# Perform net diag query
+
+r1_rloc = r1.get_rloc_ip_addr()
+r2_rloc = r2.get_rloc_ip_addr()
+
+# Get vendor name (TLV 27)
+
+result = r2.cli('networkdiagnostic get', r1_rloc, VENDOR_NAME_TLV)
+verify(len(result) == 2)
+verify(result[1].startswith("Vendor Name:"))
+verify(result[1].split(':')[1].strip() == r1.get_vendor_name())
+
+# Get vendor model (TLV 28)
+
+result = r2.cli('networkdiagnostic get', r1_rloc, VENDOR_MODEL_TLV)
+verify(len(result) == 2)
+verify(result[1].startswith("Vendor Model:"))
+verify(result[1].split(':')[1].strip() == r1.get_vendor_model())
+
+# Get vendor sw version (TLV 29)
+
+result = r2.cli('networkdiagnostic get', r1_rloc, VENDOR_SW_VERSION_TLV)
+verify(len(result) == 2)
+verify(result[1].startswith("Vendor SW Version:"))
+verify(result[1].split(':')[1].strip() == r1.get_vendor_sw_version())
+
+# Get thread stack version (TLV 30)
+
+result = r2.cli('networkdiagnostic get', r1_rloc, THREAD_STACK_VERSION_TLV)
+verify(len(result) == 2)
+verify(result[1].startswith("Thread Stack Version:"))
+verify(r1.get_version().startswith(result[1].split(':', 1)[1].strip()))
+
+# Get all three TLVs (now from `r1`)
+
+result = r1.cli('networkdiagnostic get', r2_rloc, VENDOR_NAME_TLV, VENDOR_MODEL_TLV, VENDOR_SW_VERSION_TLV,
+                THREAD_STACK_VERSION_TLV)
+verify(len(result) == 5)
+for line in result[1:]:
+    if line.startswith("Vendor Name:"):
+        verify(line.split(':')[1].strip() == r2.get_vendor_name())
+    elif line.startswith("Vendor Model:"):
+        verify(line.split(':')[1].strip() == r2.get_vendor_model())
+    elif line.startswith("Vendor SW Version:"):
+        verify(line.split(':')[1].strip() == r2.get_vendor_sw_version())
+    elif line.startswith("Thread Stack Version:"):
+        verify(r2.get_version().startswith(line.split(':', 1)[1].strip()))
+    else:
+        verify(False)
+
+# -----------------------------------------------------------------------------------------------------------------------
+# Test finished
+
+cli.Node.finalize_all_nodes()
+
+print('\'{}\' passed.'.format(test_name))
diff --git a/tests/toranj/cli/test-601-channel-manager-channel-change.py b/tests/toranj/cli/test-601-channel-manager-channel-change.py
index cb67d64..82be9f7 100755
--- a/tests/toranj/cli/test-601-channel-manager-channel-change.py
+++ b/tests/toranj/cli/test-601-channel-manager-channel-change.py
@@ -154,7 +154,7 @@
 r1.cli('channel manager change 17')
 time.sleep(5 / speedup)
 verify_within(check_channel_on_all_nodes, 10)
-channael = 18
+channel = 18
 r2.cli('channel manager change', channel)
 verify_within(check_channel_on_all_nodes, 10)
 
diff --git a/tests/toranj/ncp/test-030-slaac-address-ncp.py b/tests/toranj/ncp/test-030-slaac-address-ncp.py
index cbcc5e2..6bca119 100644
--- a/tests/toranj/ncp/test-030-slaac-address-ncp.py
+++ b/tests/toranj/ncp/test-030-slaac-address-ncp.py
@@ -173,7 +173,7 @@
 slaac_addrs = [node.find_ip6_address_with_prefix(PREFIX) for node in all_nodes]
 
 # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-# Check recovery after reseting r1 and c1 (same SLAAC address to be added)
+# Check recovery after resetting r1 and c1 (same SLAAC address to be added)
 
 r1.reset()
 wpan.verify_within(check_prefix_and_slaac_address_are_added, WAIT_INTERVAL)
diff --git a/tests/toranj/ncp/wpan.py b/tests/toranj/ncp/wpan.py
index 1dc313a..ffe20a2 100644
--- a/tests/toranj/ncp/wpan.py
+++ b/tests/toranj/ncp/wpan.py
@@ -747,7 +747,7 @@
             while asyncore.socket_map:
                 elapsed_time = time.time() - start_time
                 if elapsed_time > timeout:
-                    print('Performing aysnc tx/tx took too long ({}>{} sec)'.format(elapsed_time, timeout))
+                    print('Performing async tx/tx took too long ({}>{} sec)'.format(elapsed_time, timeout))
                     raise Node._NodeError('perform_tx_rx timed out ({}>{} sec)'.format(elapsed_time, timeout))
                 # perform a single asyncore loop
                 asyncore.loop(timeout=0.5, count=1)
diff --git a/tests/toranj/openthread-core-toranj-config.h b/tests/toranj/openthread-core-toranj-config.h
index 5ae92b2..6f34920 100644
--- a/tests/toranj/openthread-core-toranj-config.h
+++ b/tests/toranj/openthread-core-toranj-config.h
@@ -76,6 +76,14 @@
 #define OPENTHREAD_CONFIG_BORDER_ROUTING_ENABLE 1
 
 /**
+ * @def OPENTHREAD_CONFIG_NAT64_BORDER_ROUTING_ENABLE
+ *
+ * Define to 1 to enable NAT64 support in Border Routing Manager.
+ *
+ */
+#define OPENTHREAD_CONFIG_NAT64_BORDER_ROUTING_ENABLE 1
+
+/**
  * @def OPENTHREAD_CONFIG_IP6_BR_COUNTERS_ENABLE
  *
  * Define as 1 to enable IPv6 Border Routing counters.
@@ -557,6 +565,52 @@
  */
 #define OPENTHREAD_CONFIG_CLI_REGISTER_IP6_RECV_CALLBACK 1
 
+/**
+ * @def OPENTHREAD_CONFIG_MLE_PARENT_RESPONSE_CALLBACK_API_ENABLE
+ *
+ * Define as 1 to support `otThreadRegisterParentResponseCallback()` API which registers a callback to notify user
+ * of received Parent Response message(s) during attach.
+ *
+ */
+#define OPENTHREAD_CONFIG_MLE_PARENT_RESPONSE_CALLBACK_API_ENABLE 1
+
+/**
+ * @def OPENTHREAD_CONFIG_NET_DIAG_VENDOR_NAME
+ *
+ * Specifies the default Vendor Name string.
+ *
+ */
+#ifndef OPENTHREAD_CONFIG_NET_DIAG_VENDOR_NAME
+#define OPENTHREAD_CONFIG_NET_DIAG_VENDOR_NAME "OpenThread by Google Nest"
+#endif
+
+/**
+ * @def OPENTHREAD_CONFIG_NET_DIAG_VENDOR_MODEL
+ *
+ * Specifies the default Vendor Model string.
+ *
+ */
+#ifndef OPENTHREAD_CONFIG_NET_DIAG_VENDOR_MODEL
+#define OPENTHREAD_CONFIG_NET_DIAG_VENDOR_MODEL "Toranj Simulation"
+#endif
+
+/**
+ * @def OPENTHREAD_CONFIG_NET_DIAG_VENDOR_SW_VERSION
+ *
+ * Specifies the default Vendor SW Version string.
+ *
+ */
+#ifndef OPENTHREAD_CONFIG_NET_DIAG_VENDOR_SW_VERSION
+#define OPENTHREAD_CONFIG_NET_DIAG_VENDOR_SW_VERSION "OT-simul-toranj"
+#endif
+
+/**
+ * @def OPENTHREAD_CONFIG_NET_DIAG_VENDOR_INFO_SET_API_ENABLE
+ *
+ * Define as 1 to add APIs to allow Vendor Name, Model, SW Version to change at run-time.
+ */
+#define OPENTHREAD_CONFIG_NET_DIAG_VENDOR_INFO_SET_API_ENABLE OPENTHREAD_FTD
+
 #if OPENTHREAD_RADIO
 /**
  * @def OPENTHREAD_CONFIG_MAC_SOFTWARE_ACK_TIMEOUT_ENABLE
diff --git a/tests/toranj/start.sh b/tests/toranj/start.sh
index d2f0197..2bc8fa9 100755
--- a/tests/toranj/start.sh
+++ b/tests/toranj/start.sh
@@ -184,6 +184,7 @@
     run cli/test-017-network-data-versions.py
     run cli/test-018-next-hop-and-path-cost.py
     run cli/test-019-netdata-context-id.py
+    run cli/test-020-net-diag-vendor-info.py
     run cli/test-400-srp-client-server.py
     run cli/test-601-channel-manager-channel-change.py
     # Skip the "channel-select" test on a TREL only radio link, since it
diff --git a/tests/unit/CMakeLists.txt b/tests/unit/CMakeLists.txt
index aeda3cc..ca963d2 100644
--- a/tests/unit/CMakeLists.txt
+++ b/tests/unit/CMakeLists.txt
@@ -30,7 +30,6 @@
     ${PROJECT_SOURCE_DIR}/include
     ${PROJECT_SOURCE_DIR}/src
     ${PROJECT_SOURCE_DIR}/src/core
-    ${PROJECT_SOURCE_DIR}/examples/platforms/simulation
 )
 
 set(COMMON_COMPILE_OPTIONS
@@ -258,6 +257,27 @@
 
 add_test(NAME ot-test-dns COMMAND ot-test-dns)
 
+add_executable(ot-test-dns-client
+    test_dns_client.cpp
+)
+
+target_include_directories(ot-test-dns-client
+    PRIVATE
+        ${COMMON_INCLUDES}
+)
+
+target_compile_options(ot-test-dns-client
+    PRIVATE
+        ${COMMON_COMPILE_OPTIONS}
+)
+
+target_link_libraries(ot-test-dns-client
+    PRIVATE
+        ${COMMON_LIBS}
+)
+
+add_test(NAME ot-test-dns-client COMMAND ot-test-dns-client)
+
 add_executable(ot-test-dso
     test_dso.cpp
 )
@@ -1032,10 +1052,17 @@
 add_executable(ot-test-toolchain
     test_toolchain.cpp test_toolchain_c.c
 )
+
 target_include_directories(ot-test-toolchain
     PRIVATE
         ${COMMON_INCLUDES}
 )
+
+target_link_libraries(ot-test-toolchain
+    PRIVATE
+        ${COMMON_LIBS}
+)
+
 add_test(NAME ot-test-toolchain COMMAND ot-test-toolchain)
 
 target_include_directories(ot-test-timer
diff --git a/tests/unit/test_aes.cpp b/tests/unit/test_aes.cpp
index e88b1e2..4ed31ed 100644
--- a/tests/unit/test_aes.cpp
+++ b/tests/unit/test_aes.cpp
@@ -234,7 +234,7 @@
 
         VerifyOrQuit(message->GetLength() == msgLength + kTagLength);
 
-        // Decrpt in place
+        // Decrypt in place
         aesCcm.Init(kHeaderLength, msgLength - kHeaderLength, kTagLength, kNonce, sizeof(kNonce));
         aesCcm.Header(header);
         aesCcm.Payload(*message, kHeaderLength, msgLength - kHeaderLength, ot::Crypto::AesCcm::kDecrypt);
diff --git a/tests/unit/test_dns.cpp b/tests/unit/test_dns.cpp
index 6df6b3b..089b486 100644
--- a/tests/unit/test_dns.cpp
+++ b/tests/unit/test_dns.cpp
@@ -1258,7 +1258,7 @@
     const uint8_t kInvalidEncodedTxt1[] = {4, 'a', '=', 'b'}; // Incorrect length
 
     // Special encoded txt data with zero strings and string starting
-    // with '=' (missing key) whcih should be skipped over silently.
+    // with '=' (missing key) which should be skipped over silently.
     const uint8_t kSpecialEncodedTxt[] = {0, 0, 3, 'A', '=', 'B', 2, '=', 'C', 3, 'D', '=', 'E', 3, '=', '1', '2'};
 
     const Dns::TxtEntry kTxtEntries[] = {
diff --git a/tests/unit/test_dns_client.cpp b/tests/unit/test_dns_client.cpp
new file mode 100644
index 0000000..ad36def
--- /dev/null
+++ b/tests/unit/test_dns_client.cpp
@@ -0,0 +1,902 @@
+/*
+ *  Copyright (c) 2023, The OpenThread Authors.
+ *  All rights reserved.
+ *
+ *  Redistribution and use in source and binary forms, with or without
+ *  modification, are permitted provided that the following conditions are met:
+ *  1. Redistributions of source code must retain the above copyright
+ *     notice, this list of conditions and the following disclaimer.
+ *  2. Redistributions in binary form must reproduce the above copyright
+ *     notice, this list of conditions and the following disclaimer in the
+ *     documentation and/or other materials provided with the distribution.
+ *  3. Neither the name of the copyright holder nor the
+ *     names of its contributors may be used to endorse or promote products
+ *     derived from this software without specific prior written permission.
+ *
+ *  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ *  AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ *  IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ *  ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ *  LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ *  CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ *  SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ *  INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ *  CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ *  ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ *  POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include <openthread/config.h>
+
+#include "test_platform.h"
+#include "test_util.hpp"
+
+#include <openthread/dns_client.h>
+#include <openthread/srp_client.h>
+#include <openthread/srp_server.h>
+#include <openthread/thread.h>
+
+#include "common/arg_macros.hpp"
+#include "common/array.hpp"
+#include "common/instance.hpp"
+#include "common/string.hpp"
+#include "common/time.hpp"
+
+#if OPENTHREAD_CONFIG_DNS_CLIENT_ENABLE && OPENTHREAD_CONFIG_DNS_CLIENT_SERVICE_DISCOVERY_ENABLE &&                 \
+    OPENTHREAD_CONFIG_DNS_CLIENT_DEFAULT_SERVER_ADDRESS_AUTO_SET_ENABLE && OPENTHREAD_CONFIG_DNSSD_SERVER_ENABLE && \
+    OPENTHREAD_CONFIG_SRP_SERVER_ENABLE && OPENTHREAD_CONFIG_SRP_CLIENT_ENABLE &&                                   \
+    !OPENTHREAD_CONFIG_TIME_SYNC_ENABLE && !OPENTHREAD_PLATFORM_POSIX
+#define ENABLE_DNS_TEST 1
+#else
+#define ENABLE_DNS_TEST 0
+#endif
+
+#if ENABLE_DNS_TEST
+
+using namespace ot;
+
+// Logs a message and adds current time (sNow) as "<hours>:<min>:<secs>.<msec>"
+#define Log(...)                                                                                          \
+    printf("%02u:%02u:%02u.%03u " OT_FIRST_ARG(__VA_ARGS__) "\n", (sNow / 36000000), (sNow / 60000) % 60, \
+           (sNow / 1000) % 60, sNow % 1000 OT_REST_ARGS(__VA_ARGS__))
+
+static constexpr uint16_t kMaxRaSize = 800;
+
+static ot::Instance *sInstance;
+
+static uint32_t sNow = 0;
+static uint32_t sAlarmTime;
+static bool     sAlarmOn = false;
+
+static otRadioFrame sRadioTxFrame;
+static uint8_t      sRadioTxFramePsdu[OT_RADIO_FRAME_MAX_SIZE];
+static bool         sRadioTxOngoing = false;
+
+//----------------------------------------------------------------------------------------------------------------------
+// Function prototypes
+
+void ProcessRadioTxAndTasklets(void);
+void AdvanceTime(uint32_t aDuration);
+
+//----------------------------------------------------------------------------------------------------------------------
+// `otPlatRadio`
+
+extern "C" {
+
+otError otPlatRadioTransmit(otInstance *, otRadioFrame *)
+{
+    sRadioTxOngoing = true;
+
+    return OT_ERROR_NONE;
+}
+
+otRadioFrame *otPlatRadioGetTransmitBuffer(otInstance *) { return &sRadioTxFrame; }
+
+//----------------------------------------------------------------------------------------------------------------------
+// `otPlatAlaram`
+
+void otPlatAlarmMilliStop(otInstance *) { sAlarmOn = false; }
+
+void otPlatAlarmMilliStartAt(otInstance *, uint32_t aT0, uint32_t aDt)
+{
+    sAlarmOn   = true;
+    sAlarmTime = aT0 + aDt;
+}
+
+uint32_t otPlatAlarmMilliGetNow(void) { return sNow; }
+
+//----------------------------------------------------------------------------------------------------------------------
+
+Array<void *, 500> sHeapAllocatedPtrs;
+
+#if OPENTHREAD_CONFIG_HEAP_EXTERNAL_ENABLE
+void *otPlatCAlloc(size_t aNum, size_t aSize)
+{
+    void *ptr = calloc(aNum, aSize);
+
+    SuccessOrQuit(sHeapAllocatedPtrs.PushBack(ptr));
+
+    return ptr;
+}
+
+void otPlatFree(void *aPtr)
+{
+    if (aPtr != nullptr)
+    {
+        void **entry = sHeapAllocatedPtrs.Find(aPtr);
+
+        VerifyOrQuit(entry != nullptr, "A heap allocated item is freed twice");
+        sHeapAllocatedPtrs.Remove(*entry);
+    }
+
+    free(aPtr);
+}
+#endif
+
+#if OPENTHREAD_CONFIG_LOG_OUTPUT == OPENTHREAD_CONFIG_LOG_OUTPUT_PLATFORM_DEFINED
+void otPlatLog(otLogLevel aLogLevel, otLogRegion aLogRegion, const char *aFormat, ...)
+{
+    OT_UNUSED_VARIABLE(aLogLevel);
+    OT_UNUSED_VARIABLE(aLogRegion);
+
+    va_list args;
+
+    printf("   ");
+    va_start(args, aFormat);
+    vprintf(aFormat, args);
+    va_end(args);
+    printf("\n");
+}
+#endif
+
+} // extern "C"
+
+//---------------------------------------------------------------------------------------------------------------------
+
+void ProcessRadioTxAndTasklets(void)
+{
+    do
+    {
+        if (sRadioTxOngoing)
+        {
+            sRadioTxOngoing = false;
+            otPlatRadioTxStarted(sInstance, &sRadioTxFrame);
+            otPlatRadioTxDone(sInstance, &sRadioTxFrame, nullptr, OT_ERROR_NONE);
+        }
+
+        otTaskletsProcess(sInstance);
+    } while (otTaskletsArePending(sInstance));
+}
+
+void AdvanceTime(uint32_t aDuration)
+{
+    uint32_t time = sNow + aDuration;
+
+    Log("AdvanceTime for %u.%03u", aDuration / 1000, aDuration % 1000);
+
+    while (TimeMilli(sAlarmTime) <= TimeMilli(time))
+    {
+        ProcessRadioTxAndTasklets();
+        sNow = sAlarmTime;
+        otPlatAlarmMilliFired(sInstance);
+    }
+
+    ProcessRadioTxAndTasklets();
+    sNow = time;
+}
+
+void InitTest(void)
+{
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Initialize OT instance.
+
+    sNow      = 0;
+    sInstance = static_cast<Instance *>(testInitInstance());
+
+    memset(&sRadioTxFrame, 0, sizeof(sRadioTxFrame));
+    sRadioTxFrame.mPsdu = sRadioTxFramePsdu;
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Initialize Border Router and start Thread operation.
+
+    SuccessOrQuit(otLinkSetPanId(sInstance, 0x1234));
+    SuccessOrQuit(otIp6SetEnabled(sInstance, true));
+    SuccessOrQuit(otThreadSetEnabled(sInstance, true));
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Ensure device starts as leader.
+
+    AdvanceTime(10000);
+
+    VerifyOrQuit(otThreadGetDeviceRole(sInstance) == OT_DEVICE_ROLE_LEADER);
+}
+
+void FinalizeTest(void)
+{
+    SuccessOrQuit(otIp6SetEnabled(sInstance, false));
+    SuccessOrQuit(otThreadSetEnabled(sInstance, false));
+    // Make sure there is no message/buffer leak
+    VerifyOrQuit(sInstance->Get<MessagePool>().GetFreeBufferCount() ==
+                 sInstance->Get<MessagePool>().GetTotalBufferCount());
+    SuccessOrQuit(otInstanceErasePersistentInfo(sInstance));
+    testFreeInstance(sInstance);
+}
+
+//---------------------------------------------------------------------------------------------------------------------
+
+static const char kHostName[]     = "elden";
+static const char kHostFullName[] = "elden.default.service.arpa.";
+
+static const char kService1Name[]     = "_srv._udp";
+static const char kService1FullName[] = "_srv._udp.default.service.arpa.";
+static const char kInstance1Label[]   = "srv-instance";
+
+static const char kService2Name[]     = "_game._udp";
+static const char kService2FullName[] = "_game._udp.default.service.arpa.";
+static const char kInstance2Label[]   = "last-ninja";
+
+void PrepareService1(Srp::Client::Service &aService)
+{
+    static const char          kSub1[]       = "_sub1";
+    static const char          kSub2[]       = "_V1234567";
+    static const char          kSub3[]       = "_XYZWS";
+    static const char         *kSubLabels[]  = {kSub1, kSub2, kSub3, nullptr};
+    static const char          kTxtKey1[]    = "ABCD";
+    static const uint8_t       kTxtValue1[]  = {'a', '0'};
+    static const char          kTxtKey2[]    = "Z0";
+    static const uint8_t       kTxtValue2[]  = {'1', '2', '3'};
+    static const char          kTxtKey3[]    = "D";
+    static const uint8_t       kTxtValue3[]  = {0};
+    static const otDnsTxtEntry kTxtEntries[] = {
+        {kTxtKey1, kTxtValue1, sizeof(kTxtValue1)},
+        {kTxtKey2, kTxtValue2, sizeof(kTxtValue2)},
+        {kTxtKey3, kTxtValue3, sizeof(kTxtValue3)},
+    };
+
+    memset(&aService, 0, sizeof(aService));
+    aService.mName          = kService1Name;
+    aService.mInstanceName  = kInstance1Label;
+    aService.mSubTypeLabels = kSubLabels;
+    aService.mTxtEntries    = kTxtEntries;
+    aService.mNumTxtEntries = 3;
+    aService.mPort          = 777;
+    aService.mWeight        = 1;
+    aService.mPriority      = 2;
+}
+
+void PrepareService2(Srp::Client::Service &aService)
+{
+    static const char  kSub4[]       = "_44444444";
+    static const char *kSubLabels2[] = {kSub4, nullptr};
+
+    memset(&aService, 0, sizeof(aService));
+    aService.mName          = kService2Name;
+    aService.mInstanceName  = kInstance2Label;
+    aService.mSubTypeLabels = kSubLabels2;
+    aService.mTxtEntries    = nullptr;
+    aService.mNumTxtEntries = 0;
+    aService.mPort          = 555;
+    aService.mWeight        = 0;
+    aService.mPriority      = 3;
+}
+
+void ValidateHost(Srp::Server &aServer, const char *aHostName)
+{
+    // Validate that only a host with `aHostName` is
+    // registered on SRP server.
+
+    const Srp::Server::Host *host;
+    const char              *name;
+
+    Log("ValidateHost()");
+
+    host = aServer.GetNextHost(nullptr);
+    VerifyOrQuit(host != nullptr);
+
+    name = host->GetFullName();
+    Log("Hostname: %s", name);
+
+    VerifyOrQuit(StringStartsWith(name, aHostName, kStringCaseInsensitiveMatch));
+    VerifyOrQuit(name[strlen(aHostName)] == '.');
+
+    // Only one host on server
+    VerifyOrQuit(aServer.GetNextHost(host) == nullptr);
+}
+
+//---------------------------------------------------------------------------------------------------------------------
+
+void LogServiceInfo(const Dns::Client::ServiceInfo &aInfo)
+{
+    Log("   TTL: %u", aInfo.mTtl);
+    Log("   Port: %u", aInfo.mPort);
+    Log("   Weight: %u", aInfo.mWeight);
+    Log("   HostName: %s", aInfo.mHostNameBuffer);
+    Log("   HostAddr: %s", AsCoreType(&aInfo.mHostAddress).ToString().AsCString());
+    Log("   TxtDataLength: %u", aInfo.mTxtDataSize);
+    Log("   TxtDataTTL: %u", aInfo.mTxtDataTtl);
+}
+
+const char *ServiceModeToString(Dns::Client::QueryConfig::ServiceMode aMode)
+{
+    static const char *const kServiceModeStrings[] = {
+        "unspec",      // kServiceModeUnspecified     (0)
+        "srv",         // kServiceModeSrv             (1)
+        "txt",         // kServiceModeTxt             (2)
+        "srv_txt",     // kServiceModeSrvTxt          (3)
+        "srv_txt_sep", // kServiceModeSrvTxtSeparate  (4)
+        "srv_txt_opt", // kServiceModeSrvTxtOptimize  (5)
+    };
+
+    static_assert(Dns::Client::QueryConfig::kServiceModeUnspecified == 0, "Unspecified value is incorrect");
+    static_assert(Dns::Client::QueryConfig::kServiceModeSrv == 1, "Srv value is incorrect");
+    static_assert(Dns::Client::QueryConfig::kServiceModeTxt == 2, "Txt value is incorrect");
+    static_assert(Dns::Client::QueryConfig::kServiceModeSrvTxt == 3, "SrvTxt value is incorrect");
+    static_assert(Dns::Client::QueryConfig::kServiceModeSrvTxtSeparate == 4, "SrvTxtSeparate value is incorrect");
+    static_assert(Dns::Client::QueryConfig::kServiceModeSrvTxtOptimize == 5, "SrvTxtOptimize value is incorrect");
+
+    return kServiceModeStrings[aMode];
+}
+
+//- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+struct BrowseInfo
+{
+    void Reset(void) { mCallbackCount = 0; }
+
+    uint16_t mCallbackCount;
+    Error    mError;
+    char     mServiceName[Dns::Name::kMaxNameSize];
+    uint16_t mNumInstances;
+};
+
+static BrowseInfo sBrowseInfo;
+
+void BrowseCallback(otError aError, const otDnsBrowseResponse *aResponse, void *aContext)
+{
+    const Dns::Client::BrowseResponse &response = AsCoreType(aResponse);
+
+    Log("BrowseCallback");
+    Log("   Error: %s", ErrorToString(aError));
+
+    VerifyOrQuit(aContext == sInstance);
+
+    sBrowseInfo.mCallbackCount++;
+    sBrowseInfo.mError = aError;
+
+    SuccessOrExit(aError);
+
+    SuccessOrQuit(response.GetServiceName(sBrowseInfo.mServiceName, sizeof(sBrowseInfo.mServiceName)));
+    Log("   ServiceName: %s", sBrowseInfo.mServiceName);
+
+    for (uint16_t index = 0;; index++)
+    {
+        char  instLabel[Dns::Name::kMaxLabelSize];
+        Error error;
+
+        error = response.GetServiceInstance(index, instLabel, sizeof(instLabel));
+
+        if (error == kErrorNotFound)
+        {
+            sBrowseInfo.mNumInstances = index;
+            break;
+        }
+
+        SuccessOrQuit(error);
+
+        Log("  %2u) %s", index + 1, instLabel);
+    }
+
+exit:
+    return;
+}
+
+//- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+static constexpr uint8_t  kMaxHostAddresses = 10;
+static constexpr uint16_t kMaxTxtBuffer     = 256;
+
+struct ResolveServiceInfo
+{
+    void Reset(void)
+    {
+        memset(this, 0, sizeof(*this));
+        mInfo.mHostNameBuffer     = mNameBuffer;
+        mInfo.mHostNameBufferSize = sizeof(mNameBuffer);
+        mInfo.mTxtData            = mTxtBuffer;
+        mInfo.mTxtDataSize        = sizeof(mTxtBuffer);
+    };
+
+    uint16_t                 mCallbackCount;
+    Error                    mError;
+    Dns::Client::ServiceInfo mInfo;
+    char                     mNameBuffer[Dns::Name::kMaxNameSize];
+    uint8_t                  mTxtBuffer[kMaxTxtBuffer];
+    Ip6::Address             mHostAddresses[kMaxHostAddresses];
+    uint8_t                  mNumHostAddresses;
+};
+
+static ResolveServiceInfo sResolveServiceInfo;
+
+void ServiceCallback(otError aError, const otDnsServiceResponse *aResponse, void *aContext)
+{
+    const Dns::Client::ServiceResponse &response = AsCoreType(aResponse);
+    char                                instLabel[Dns::Name::kMaxLabelSize];
+    char                                serviceName[Dns::Name::kMaxNameSize];
+
+    Log("ServiceCallback");
+    Log("   Error: %s", ErrorToString(aError));
+
+    VerifyOrQuit(aContext == sInstance);
+
+    SuccessOrQuit(response.GetServiceName(instLabel, sizeof(instLabel), serviceName, sizeof(serviceName)));
+    Log("   InstLabel: %s", instLabel);
+    Log("   ServiceName: %s", serviceName);
+
+    sResolveServiceInfo.mCallbackCount++;
+    sResolveServiceInfo.mError = aError;
+
+    SuccessOrExit(aError);
+    SuccessOrQuit(response.GetServiceInfo(sResolveServiceInfo.mInfo));
+
+    for (uint8_t index = 0; index < kMaxHostAddresses; index++)
+    {
+        Error    error;
+        uint32_t ttl;
+
+        error = response.GetHostAddress(sResolveServiceInfo.mInfo.mHostNameBuffer, index,
+                                        sResolveServiceInfo.mHostAddresses[index], ttl);
+
+        if (error == kErrorNotFound)
+        {
+            sResolveServiceInfo.mNumHostAddresses = index;
+            break;
+        }
+
+        SuccessOrQuit(error);
+    }
+
+    LogServiceInfo(sResolveServiceInfo.mInfo);
+    Log("   NumHostAddresses: %u", sResolveServiceInfo.mNumHostAddresses);
+
+    for (uint8_t index = 0; index < sResolveServiceInfo.mNumHostAddresses; index++)
+    {
+        Log("      %s", sResolveServiceInfo.mHostAddresses[index].ToString().AsCString());
+    }
+
+exit:
+    return;
+}
+
+//----------------------------------------------------------------------------------------------------------------------
+
+void TestDnsClient(void)
+{
+    static constexpr uint8_t kNumAddresses = 2;
+
+    static const char *const kAddresses[kNumAddresses] = {"2001::beef:cafe", "fd00:1234:5678:9abc::1"};
+
+    const Dns::Client::QueryConfig::ServiceMode kServiceModes[] = {
+        Dns::Client::QueryConfig::kServiceModeSrv,
+        Dns::Client::QueryConfig::kServiceModeTxt,
+        Dns::Client::QueryConfig::kServiceModeSrvTxt,
+        Dns::Client::QueryConfig::kServiceModeSrvTxtSeparate,
+        Dns::Client::QueryConfig::kServiceModeSrvTxtOptimize,
+    };
+
+    Array<Ip6::Address, kNumAddresses> addresses;
+    Srp::Server                       *srpServer;
+    Srp::Client                       *srpClient;
+    Srp::Client::Service               service1;
+    Srp::Client::Service               service2;
+    Dns::Client                       *dnsClient;
+    Dns::Client::QueryConfig           queryConfig;
+    Dns::ServiceDiscovery::Server     *dnsServer;
+    uint16_t                           heapAllocations;
+
+    Log("--------------------------------------------------------------------------------------------");
+    Log("TestDnsClient");
+
+    InitTest();
+
+    for (const char *addrString : kAddresses)
+    {
+        otNetifAddress netifAddr;
+
+        memset(&netifAddr, 0, sizeof(netifAddr));
+        SuccessOrQuit(AsCoreType(&netifAddr.mAddress).FromString(addrString));
+        netifAddr.mPrefixLength  = 64;
+        netifAddr.mAddressOrigin = OT_ADDRESS_ORIGIN_MANUAL;
+        netifAddr.mPreferred     = true;
+        netifAddr.mValid         = true;
+        SuccessOrQuit(otIp6AddUnicastAddress(sInstance, &netifAddr));
+
+        SuccessOrQuit(addresses.PushBack(AsCoreType(&netifAddr.mAddress)));
+    }
+
+    srpServer = &sInstance->Get<Srp::Server>();
+    srpClient = &sInstance->Get<Srp::Client>();
+    dnsClient = &sInstance->Get<Dns::Client>();
+    dnsServer = &sInstance->Get<Dns::ServiceDiscovery::Server>();
+
+    heapAllocations = sHeapAllocatedPtrs.GetLength();
+
+    PrepareService1(service1);
+    PrepareService2(service2);
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Start SRP server.
+
+    SuccessOrQuit(srpServer->SetAddressMode(Srp::Server::kAddressModeUnicast));
+    VerifyOrQuit(srpServer->GetState() == Srp::Server::kStateDisabled);
+
+    srpServer->SetEnabled(true);
+    VerifyOrQuit(srpServer->GetState() != Srp::Server::kStateDisabled);
+
+    AdvanceTime(10000);
+    VerifyOrQuit(srpServer->GetState() == Srp::Server::kStateRunning);
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Start SRP client.
+
+    srpClient->EnableAutoStartMode(nullptr, nullptr);
+    VerifyOrQuit(srpClient->IsAutoStartModeEnabled());
+
+    AdvanceTime(2000);
+    VerifyOrQuit(srpClient->IsRunning());
+
+    SuccessOrQuit(srpClient->SetHostName(kHostName));
+    SuccessOrQuit(srpClient->EnableAutoHostAddress());
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Register two services on SRP.
+
+    SuccessOrQuit(srpClient->AddService(service1));
+    SuccessOrQuit(srpClient->AddService(service2));
+
+    AdvanceTime(2 * 1000);
+
+    VerifyOrQuit(service1.GetState() == Srp::Client::kRegistered);
+    VerifyOrQuit(service2.GetState() == Srp::Client::kRegistered);
+    ValidateHost(*srpServer, kHostName);
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ");
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Check DNS Client's default config
+
+    VerifyOrQuit(dnsClient->GetDefaultConfig().GetServiceMode() ==
+                 Dns::Client::QueryConfig::kServiceModeSrvTxtOptimize);
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Validate DNS Client `Browse()`
+
+    sBrowseInfo.Reset();
+    Log("Browse(%s)", kService1FullName);
+    SuccessOrQuit(dnsClient->Browse(kService1FullName, BrowseCallback, sInstance));
+    AdvanceTime(100);
+    VerifyOrQuit(sBrowseInfo.mCallbackCount == 1);
+    SuccessOrQuit(sBrowseInfo.mError);
+    VerifyOrQuit(sBrowseInfo.mNumInstances == 1);
+
+    sBrowseInfo.Reset();
+
+    Log("Browse(%s)", kService2FullName);
+    SuccessOrQuit(dnsClient->Browse(kService2FullName, BrowseCallback, sInstance));
+    AdvanceTime(100);
+    VerifyOrQuit(sBrowseInfo.mCallbackCount == 1);
+    SuccessOrQuit(sBrowseInfo.mError);
+    VerifyOrQuit(sBrowseInfo.mNumInstances == 1);
+
+    sBrowseInfo.Reset();
+    Log("Browse() for unknown service");
+    SuccessOrQuit(dnsClient->Browse("_unknown._udp.default.service.arpa.", BrowseCallback, sInstance));
+    AdvanceTime(100);
+    VerifyOrQuit(sBrowseInfo.mCallbackCount == 1);
+    VerifyOrQuit(sBrowseInfo.mError == kErrorNotFound);
+
+    Log("Issue four parallel `Browse()` at the same time");
+    sBrowseInfo.Reset();
+    SuccessOrQuit(dnsClient->Browse(kService1FullName, BrowseCallback, sInstance));
+    SuccessOrQuit(dnsClient->Browse(kService2FullName, BrowseCallback, sInstance));
+    SuccessOrQuit(dnsClient->Browse("_unknown._udp.default.service.arpa.", BrowseCallback, sInstance));
+    SuccessOrQuit(dnsClient->Browse("_unknown2._udp.default.service.arpa.", BrowseCallback, sInstance));
+    AdvanceTime(100);
+    VerifyOrQuit(sBrowseInfo.mCallbackCount == 4);
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Validate DNS Client `ResolveService()` using all service modes
+
+    for (Dns::Client::QueryConfig::ServiceMode mode : kServiceModes)
+    {
+        Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ");
+        Log("ResolveService(%s,%s) with ServiceMode: %s", kInstance1Label, kService1FullName,
+            ServiceModeToString(mode));
+
+        queryConfig.Clear();
+        queryConfig.mServiceMode = static_cast<otDnsServiceMode>(mode);
+
+        sResolveServiceInfo.Reset();
+        SuccessOrQuit(
+            dnsClient->ResolveService(kInstance1Label, kService1FullName, ServiceCallback, sInstance, &queryConfig));
+        AdvanceTime(100);
+
+        VerifyOrQuit(sResolveServiceInfo.mCallbackCount == 1);
+        SuccessOrQuit(sResolveServiceInfo.mError);
+
+        if (mode != Dns::Client::QueryConfig::kServiceModeTxt)
+        {
+            VerifyOrQuit(sResolveServiceInfo.mInfo.mTtl != 0);
+            VerifyOrQuit(sResolveServiceInfo.mInfo.mPort == service1.mPort);
+            VerifyOrQuit(sResolveServiceInfo.mInfo.mWeight == service1.mWeight);
+            VerifyOrQuit(strcmp(sResolveServiceInfo.mInfo.mHostNameBuffer, kHostFullName) == 0);
+
+            VerifyOrQuit(sResolveServiceInfo.mNumHostAddresses == kNumAddresses);
+            VerifyOrQuit(AsCoreType(&sResolveServiceInfo.mInfo.mHostAddress) == sResolveServiceInfo.mHostAddresses[0]);
+
+            for (uint8_t index = 0; index < kNumAddresses; index++)
+            {
+                VerifyOrQuit(addresses.Contains(sResolveServiceInfo.mHostAddresses[index]));
+            }
+        }
+
+        if (mode != Dns::Client::QueryConfig::kServiceModeSrv)
+        {
+            VerifyOrQuit(sResolveServiceInfo.mInfo.mTxtDataTtl != 0);
+            VerifyOrQuit(sResolveServiceInfo.mInfo.mTxtDataSize != 0);
+        }
+    }
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ");
+
+    Log("Set TestMode on server to only accept single question");
+    dnsServer->SetTestMode(Dns::ServiceDiscovery::Server::kTestModeSingleQuestionOnly);
+
+    Log("ResolveService(%s,%s) with ServiceMode %s", kInstance1Label, kService1FullName,
+        ServiceModeToString(Dns::Client::QueryConfig::kServiceModeSrvTxtOptimize));
+
+    queryConfig.Clear();
+    queryConfig.mServiceMode = static_cast<otDnsServiceMode>(Dns::Client::QueryConfig::kServiceModeSrvTxtOptimize);
+
+    sResolveServiceInfo.Reset();
+    SuccessOrQuit(
+        dnsClient->ResolveService(kInstance1Label, kService1FullName, ServiceCallback, sInstance, &queryConfig));
+    AdvanceTime(200);
+
+    VerifyOrQuit(sResolveServiceInfo.mCallbackCount == 1);
+    SuccessOrQuit(sResolveServiceInfo.mError);
+
+    // Use `kServiceModeSrvTxt` and check that server does reject two questions.
+
+    Log("ResolveService(%s,%s) with ServiceMode %s", kInstance1Label, kService1FullName,
+        ServiceModeToString(Dns::Client::QueryConfig::kServiceModeSrvTxt));
+
+    queryConfig.Clear();
+    queryConfig.mServiceMode = static_cast<otDnsServiceMode>(Dns::Client::QueryConfig::kServiceModeSrvTxt);
+
+    sResolveServiceInfo.Reset();
+    SuccessOrQuit(
+        dnsClient->ResolveService(kInstance1Label, kService1FullName, ServiceCallback, sInstance, &queryConfig));
+    AdvanceTime(200);
+
+    VerifyOrQuit(sResolveServiceInfo.mCallbackCount == 1);
+    VerifyOrQuit(sResolveServiceInfo.mError != kErrorNone);
+
+    dnsServer->SetTestMode(Dns::ServiceDiscovery::Server::kTestModeDisabled);
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Validate DNS Client `ResolveService()` using all service modes
+    // when sever does not provide any RR in the addition data section.
+
+    for (Dns::Client::QueryConfig::ServiceMode mode : kServiceModes)
+    {
+        Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ");
+        Log("Set TestMode on server to not include any RR in additional section");
+        dnsServer->SetTestMode(Dns::ServiceDiscovery::Server::kTestModeEmptyAdditionalSection);
+        Log("ResolveService(%s,%s) with ServiceMode: %s", kInstance1Label, kService1FullName,
+            ServiceModeToString(mode));
+
+        queryConfig.Clear();
+        queryConfig.mServiceMode = static_cast<otDnsServiceMode>(mode);
+
+        sResolveServiceInfo.Reset();
+        SuccessOrQuit(
+            dnsClient->ResolveService(kInstance1Label, kService1FullName, ServiceCallback, sInstance, &queryConfig));
+        AdvanceTime(100);
+
+        VerifyOrQuit(sResolveServiceInfo.mCallbackCount == 1);
+        SuccessOrQuit(sResolveServiceInfo.mError);
+
+        if (mode != Dns::Client::QueryConfig::kServiceModeTxt)
+        {
+            VerifyOrQuit(sResolveServiceInfo.mInfo.mTtl != 0);
+            VerifyOrQuit(sResolveServiceInfo.mInfo.mPort == service1.mPort);
+            VerifyOrQuit(sResolveServiceInfo.mInfo.mWeight == service1.mWeight);
+            VerifyOrQuit(strcmp(sResolveServiceInfo.mInfo.mHostNameBuffer, kHostFullName) == 0);
+        }
+
+        if (mode != Dns::Client::QueryConfig::kServiceModeSrv)
+        {
+            VerifyOrQuit(sResolveServiceInfo.mInfo.mTxtDataTtl != 0);
+            VerifyOrQuit(sResolveServiceInfo.mInfo.mTxtDataSize != 0);
+        }
+
+        // Since server is using `kTestModeEmptyAdditionalSection`, there
+        // should be no AAAA records for host address.
+
+        VerifyOrQuit(AsCoreType(&sResolveServiceInfo.mInfo.mHostAddress).IsUnspecified());
+        VerifyOrQuit(sResolveServiceInfo.mNumHostAddresses == 0);
+    }
+
+    dnsServer->SetTestMode(Dns::ServiceDiscovery::Server::kTestModeDisabled);
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Validate DNS Client `ResolveServiceAndHostAddress()` using all service modes
+    // with different TestMode configs on server:
+    // - Normal behavior when server provides AAAA records for host in
+    //   additional section.
+    // - Server provides no records in additional section. We validate that
+    //   client will send separate query to resolve host address.
+
+    for (Dns::Client::QueryConfig::ServiceMode mode : kServiceModes)
+    {
+        for (uint8_t testIter = 0; testIter <= 1; testIter++)
+        {
+            Error error;
+
+            Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ");
+
+            if (testIter == 1)
+            {
+                Log("Set TestMode on server to not include any RR in additional section");
+                dnsServer->SetTestMode(Dns::ServiceDiscovery::Server::kTestModeEmptyAdditionalSection);
+            }
+            else
+            {
+                dnsServer->SetTestMode(Dns::ServiceDiscovery::Server::kTestModeDisabled);
+            }
+
+            Log("ResolveServiceAndHostAddress(%s,%s) with ServiceMode: %s", kInstance1Label, kService1FullName,
+                ServiceModeToString(mode));
+
+            queryConfig.Clear();
+            queryConfig.mServiceMode = static_cast<otDnsServiceMode>(mode);
+
+            sResolveServiceInfo.Reset();
+            error = dnsClient->ResolveServiceAndHostAddress(kInstance1Label, kService1FullName, ServiceCallback,
+                                                            sInstance, &queryConfig);
+
+            if (mode == Dns::Client::QueryConfig::kServiceModeTxt)
+            {
+                Log("ResolveServiceAndHostAddress() with ServiceMode: %s failed correctly", ServiceModeToString(mode));
+                VerifyOrQuit(error == kErrorInvalidArgs);
+                continue;
+            }
+
+            SuccessOrQuit(error);
+
+            AdvanceTime(100);
+
+            VerifyOrQuit(sResolveServiceInfo.mCallbackCount == 1);
+            SuccessOrQuit(sResolveServiceInfo.mError);
+
+            if (mode != Dns::Client::QueryConfig::kServiceModeTxt)
+            {
+                VerifyOrQuit(sResolveServiceInfo.mInfo.mTtl != 0);
+                VerifyOrQuit(sResolveServiceInfo.mInfo.mPort == service1.mPort);
+                VerifyOrQuit(sResolveServiceInfo.mInfo.mWeight == service1.mWeight);
+                VerifyOrQuit(strcmp(sResolveServiceInfo.mInfo.mHostNameBuffer, kHostFullName) == 0);
+
+                VerifyOrQuit(sResolveServiceInfo.mNumHostAddresses == kNumAddresses);
+                VerifyOrQuit(AsCoreType(&sResolveServiceInfo.mInfo.mHostAddress) ==
+                             sResolveServiceInfo.mHostAddresses[0]);
+
+                for (uint8_t index = 0; index < kNumAddresses; index++)
+                {
+                    VerifyOrQuit(addresses.Contains(sResolveServiceInfo.mHostAddresses[index]));
+                }
+            }
+
+            if (mode != Dns::Client::QueryConfig::kServiceModeSrv)
+            {
+                VerifyOrQuit(sResolveServiceInfo.mInfo.mTxtDataTtl != 0);
+                VerifyOrQuit(sResolveServiceInfo.mInfo.mTxtDataSize != 0);
+            }
+        }
+    }
+
+    dnsServer->SetTestMode(Dns::ServiceDiscovery::Server::kTestModeDisabled);
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ");
+    Log("Set TestMode on server to not include any RR in additional section AND to only accept single question");
+    dnsServer->SetTestMode(Dns::ServiceDiscovery::Server::kTestModeEmptyAdditionalSection +
+                           Dns::ServiceDiscovery::Server::kTestModeSingleQuestionOnly);
+
+    Log("ResolveServiceAndHostAddress(%s,%s) with ServiceMode: %s", kInstance1Label, kService1FullName,
+        ServiceModeToString(Dns::Client::QueryConfig::kServiceModeSrvTxtOptimize));
+
+    queryConfig.Clear();
+    queryConfig.mServiceMode = static_cast<otDnsServiceMode>(Dns::Client::QueryConfig::kServiceModeSrvTxtOptimize);
+
+    sResolveServiceInfo.Reset();
+    SuccessOrQuit(dnsClient->ResolveServiceAndHostAddress(kInstance1Label, kService1FullName, ServiceCallback,
+                                                          sInstance, &queryConfig));
+
+    AdvanceTime(100);
+
+    VerifyOrQuit(sResolveServiceInfo.mCallbackCount == 1);
+    SuccessOrQuit(sResolveServiceInfo.mError);
+
+    VerifyOrQuit(sResolveServiceInfo.mInfo.mTtl != 0);
+    VerifyOrQuit(sResolveServiceInfo.mInfo.mPort == service1.mPort);
+    VerifyOrQuit(sResolveServiceInfo.mInfo.mWeight == service1.mWeight);
+    VerifyOrQuit(strcmp(sResolveServiceInfo.mInfo.mHostNameBuffer, kHostFullName) == 0);
+
+    VerifyOrQuit(sResolveServiceInfo.mInfo.mTxtDataTtl != 0);
+    VerifyOrQuit(sResolveServiceInfo.mInfo.mTxtDataSize != 0);
+
+    VerifyOrQuit(sResolveServiceInfo.mNumHostAddresses == kNumAddresses);
+    VerifyOrQuit(AsCoreType(&sResolveServiceInfo.mInfo.mHostAddress) == sResolveServiceInfo.mHostAddresses[0]);
+
+    for (uint8_t index = 0; index < kNumAddresses; index++)
+    {
+        VerifyOrQuit(addresses.Contains(sResolveServiceInfo.mHostAddresses[index]));
+    }
+
+    dnsServer->SetTestMode(Dns::ServiceDiscovery::Server::kTestModeDisabled);
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ");
+
+    Log("Stop DNS-SD server");
+    dnsServer->Stop();
+
+    Log("ResolveService(%s,%s) with ServiceMode %s", kInstance1Label, kService1FullName,
+        ServiceModeToString(Dns::Client::QueryConfig::kServiceModeSrvTxtSeparate));
+
+    queryConfig.Clear();
+    queryConfig.mServiceMode = static_cast<otDnsServiceMode>(Dns::Client::QueryConfig::kServiceModeSrvTxtSeparate);
+
+    sResolveServiceInfo.Reset();
+    SuccessOrQuit(
+        dnsClient->ResolveService(kInstance1Label, kService1FullName, ServiceCallback, sInstance, &queryConfig));
+    AdvanceTime(25 * 1000);
+
+    VerifyOrQuit(sResolveServiceInfo.mCallbackCount == 1);
+    VerifyOrQuit(sResolveServiceInfo.mError == kErrorResponseTimeout);
+
+    Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ");
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Disable SRP server, verify that all heap allocations by SRP server
+    // and/or by DNS Client are freed.
+
+    Log("Disabling SRP server");
+
+    srpServer->SetEnabled(false);
+    AdvanceTime(100);
+
+    VerifyOrQuit(heapAllocations == sHeapAllocatedPtrs.GetLength());
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Finalize OT instance and validate all heap allocations are freed.
+
+    Log("Finalizing OT instance");
+    FinalizeTest();
+
+    VerifyOrQuit(sHeapAllocatedPtrs.IsEmpty());
+
+    Log("End of TestDnsClient");
+}
+
+#endif // ENABLE_DNS_TEST
+
+int main(void)
+{
+#if ENABLE_DNS_TEST
+    TestDnsClient();
+    printf("All tests passed\n");
+#else
+    printf("DNS_CLIENT or DSNSSD_SERVER feature is not enabled\n");
+#endif
+
+    return 0;
+}
diff --git a/tests/unit/test_ip_address.cpp b/tests/unit/test_ip_address.cpp
index 7eb1fb1..153a2c1 100644
--- a/tests/unit/test_ip_address.cpp
+++ b/tests/unit/test_ip_address.cpp
@@ -28,7 +28,9 @@
 
 #include <limits.h>
 
+#include "common/array.hpp"
 #include "common/encoding.hpp"
+#include "common/string.hpp"
 #include "net/ip4_types.hpp"
 #include "net/ip6_address.hpp"
 
@@ -166,6 +168,56 @@
     {
         checkAddressFromString(&testVector);
     }
+
+    // Validate parsing all test vectors now as an IPv6 prefix.
+
+    for (Ip6AddressTestVector &testVector : testVectors)
+    {
+        constexpr uint16_t kMaxString = 80;
+
+        ot::Ip6::Prefix prefix;
+        char            string[kMaxString];
+        uint16_t        length;
+
+        length = ot::StringLength(testVector.mString, kMaxString);
+        memcpy(string, testVector.mString, length);
+        VerifyOrQuit(length + sizeof("/128") <= kMaxString);
+        strcpy(&string[length], "/128");
+
+        printf("%s\n", string);
+
+        VerifyOrQuit(prefix.FromString(string) == testVector.mError);
+
+        if (testVector.mError == ot::kErrorNone)
+        {
+            VerifyOrQuit(memcmp(prefix.GetBytes(), testVector.mAddr, sizeof(ot::Ip6::Address)) == 0);
+            VerifyOrQuit(prefix.GetLength() == 128);
+        }
+    }
+}
+
+void TestIp6PrefixFromString(void)
+{
+    ot::Ip6::Prefix prefix;
+
+    SuccessOrQuit(prefix.FromString("::/128"));
+    VerifyOrQuit(prefix.GetLength() == 128);
+
+    SuccessOrQuit(prefix.FromString("::/0128"));
+    VerifyOrQuit(prefix.GetLength() == 128);
+
+    SuccessOrQuit(prefix.FromString("::/5"));
+    VerifyOrQuit(prefix.GetLength() == 5);
+
+    SuccessOrQuit(prefix.FromString("::/0"));
+    VerifyOrQuit(prefix.GetLength() == 0);
+
+    VerifyOrQuit(prefix.FromString("::") == ot::kErrorParse);
+    VerifyOrQuit(prefix.FromString("::/") == ot::kErrorParse);
+    VerifyOrQuit(prefix.FromString("::/129") == ot::kErrorParse);
+    VerifyOrQuit(prefix.FromString(":: /12") == ot::kErrorParse);
+    VerifyOrQuit(prefix.FromString("::/a1") == ot::kErrorParse);
+    VerifyOrQuit(prefix.FromString("::/12 ") == ot::kErrorParse);
 }
 
 void TestIp4AddressFromString(void)
@@ -194,6 +246,78 @@
     }
 }
 
+struct CidrTestVector
+{
+    const char   *mString;
+    const uint8_t mAddr[sizeof(otIp4Address)];
+    const uint8_t mLength;
+    ot::Error     mError;
+};
+
+static void checkCidrFromString(CidrTestVector *aTestVector)
+{
+    ot::Error     error;
+    ot::Ip4::Cidr cidr;
+
+    cidr.Clear();
+
+    error = cidr.FromString(aTestVector->mString);
+
+    printf("%-42s -> %-42s\n", aTestVector->mString,
+           (error == ot::kErrorNone) ? cidr.ToString().AsCString() : "(parse error)");
+
+    VerifyOrQuit(error == aTestVector->mError, "Address::FromString returned unexpected error code");
+
+    if (error == ot::kErrorNone)
+    {
+        VerifyOrQuit(0 == memcmp(cidr.GetBytes(), aTestVector->mAddr, sizeof(aTestVector->mAddr)),
+                     "Cidr::FromString parsing failed");
+        VerifyOrQuit(cidr.mLength == aTestVector->mLength, "Cidr::FromString parsing failed");
+    }
+}
+
+void TestIp4CidrFromString(void)
+{
+    CidrTestVector testVectors[] = {
+        {"0.0.0.0/0", {0, 0, 0, 0}, 0, ot::kErrorNone},
+        {"255.255.255.255/32", {255, 255, 255, 255}, 32, ot::kErrorNone},
+        {"127.0.0.1/8", {127, 0, 0, 1}, 8, ot::kErrorNone},
+        {"1.2.3.4/24", {1, 2, 3, 4}, 24, ot::kErrorNone},
+        {"001.002.003.004/20", {1, 2, 3, 4}, 20, ot::kErrorNone},
+        {"00000127.000.000.000001/8", {127, 0, 0, 1}, 8, ot::kErrorNone},
+        // Valid suffix, invalid address
+        {"123.231.0.256/4", {0}, 0, ot::kErrorParse},    // Invalid byte value.
+        {"100123.231.0.256/4", {0}, 0, ot::kErrorParse}, // Invalid byte value.
+        {"1.22.33/4", {0}, 0, ot::kErrorParse},          // Too few bytes.
+        {"1.22.33.44.5/4", {0}, 0, ot::kErrorParse},     // Too many bytes.
+        {"a.b.c.d/4", {0}, 0, ot::kErrorParse},          // Wrong digit char.
+        {"123.23.45 .12/4", {0}, 0, ot::kErrorParse},    // Extra space.
+        {"./4", {0}, 0, ot::kErrorParse},                // Invalid.
+        // valid address, invalid suffix
+        {"1.2.3.4/33", {0}, 0, ot::kErrorParse},       // Prefix length too large
+        {"1.2.3.4/12345678", {0}, 0, ot::kErrorParse}, // Prefix length too large?
+        {"1.2.3.4/12a", {0}, 0, ot::kErrorParse},      // Extra char after prefix length.
+        {"1.2.3.4/-1", {0}, 0, ot::kErrorParse},       // Not even a non-negative integer.
+        {"1.2.3.4/3.14", {0}, 0, ot::kErrorParse},     // Not even a integer.
+        {"1.2.3.4/abcd", {0}, 0, ot::kErrorParse},     // Not even a number.
+        {"1.2.3.4/", {0}, 0, ot::kErrorParse},         // Where is the suffix?
+        {"1.2.3.4", {0}, 0, ot::kErrorParse},          // Where is the suffix?
+        // invalid address and invalid suffix
+        {"123.231.0.256/41", {0}, 0, ot::kErrorParse},     // Invalid byte value.
+        {"100123.231.0.256/abc", {0}, 0, ot::kErrorParse}, // Invalid byte value.
+        {"1.22.33", {0}, 0, ot::kErrorParse},              // Too few bytes.
+        {"1.22.33.44.5/36", {0}, 0, ot::kErrorParse},      // Too many bytes.
+        {"a.b.c.d/99", {0}, 0, ot::kErrorParse},           // Wrong digit char.
+        {"123.23.45 .12", {0}, 0, ot::kErrorParse},        // Extra space.
+        {".", {0}, 0, ot::kErrorParse},                    // Invalid.
+    };
+
+    for (CidrTestVector &testVector : testVectors)
+    {
+        checkCidrFromString(&testVector);
+    }
+}
+
 bool CheckPrefix(const ot::Ip6::Address &aAddress, const uint8_t *aPrefix, uint8_t aPrefixLength)
 {
     // Check the first aPrefixLength bits of aAddress to match the given aPrefix.
@@ -458,6 +582,182 @@
     VerifyOrQuit(!PrefixFrom("fe00::", 7).IsUniqueLocal());
 }
 
+void TestIp6PrefixTidy(void)
+{
+    struct TestVector
+    {
+        uint8_t     originalPrefix[OT_IP6_ADDRESS_SIZE];
+        const char *prefixStringAfterTidy[129];
+    };
+    const TestVector kPrefixes[] = {
+        {
+            .originalPrefix = {0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+                               0xff},
+            .prefixStringAfterTidy =
+                {
+                    "::/0",
+                    "8000::/1",
+                    "c000::/2",
+                    "e000::/3",
+                    "f000::/4",
+                    "f800::/5",
+                    "fc00::/6",
+                    "fe00::/7",
+                    "ff00::/8",
+                    "ff80::/9",
+                    "ffc0::/10",
+                    "ffe0::/11",
+                    "fff0::/12",
+                    "fff8::/13",
+                    "fffc::/14",
+                    "fffe::/15",
+                    "ffff::/16",
+                    "ffff:8000::/17",
+                    "ffff:c000::/18",
+                    "ffff:e000::/19",
+                    "ffff:f000::/20",
+                    "ffff:f800::/21",
+                    "ffff:fc00::/22",
+                    "ffff:fe00::/23",
+                    "ffff:ff00::/24",
+                    "ffff:ff80::/25",
+                    "ffff:ffc0::/26",
+                    "ffff:ffe0::/27",
+                    "ffff:fff0::/28",
+                    "ffff:fff8::/29",
+                    "ffff:fffc::/30",
+                    "ffff:fffe::/31",
+                    "ffff:ffff::/32",
+                    "ffff:ffff:8000::/33",
+                    "ffff:ffff:c000::/34",
+                    "ffff:ffff:e000::/35",
+                    "ffff:ffff:f000::/36",
+                    "ffff:ffff:f800::/37",
+                    "ffff:ffff:fc00::/38",
+                    "ffff:ffff:fe00::/39",
+                    "ffff:ffff:ff00::/40",
+                    "ffff:ffff:ff80::/41",
+                    "ffff:ffff:ffc0::/42",
+                    "ffff:ffff:ffe0::/43",
+                    "ffff:ffff:fff0::/44",
+                    "ffff:ffff:fff8::/45",
+                    "ffff:ffff:fffc::/46",
+                    "ffff:ffff:fffe::/47",
+                    "ffff:ffff:ffff::/48",
+                    "ffff:ffff:ffff:8000::/49",
+                    "ffff:ffff:ffff:c000::/50",
+                    "ffff:ffff:ffff:e000::/51",
+                    "ffff:ffff:ffff:f000::/52",
+                    "ffff:ffff:ffff:f800::/53",
+                    "ffff:ffff:ffff:fc00::/54",
+                    "ffff:ffff:ffff:fe00::/55",
+                    "ffff:ffff:ffff:ff00::/56",
+                    "ffff:ffff:ffff:ff80::/57",
+                    "ffff:ffff:ffff:ffc0::/58",
+                    "ffff:ffff:ffff:ffe0::/59",
+                    "ffff:ffff:ffff:fff0::/60",
+                    "ffff:ffff:ffff:fff8::/61",
+                    "ffff:ffff:ffff:fffc::/62",
+                    "ffff:ffff:ffff:fffe::/63",
+                    "ffff:ffff:ffff:ffff::/64",
+                    "ffff:ffff:ffff:ffff:8000::/65",
+                    "ffff:ffff:ffff:ffff:c000::/66",
+                    "ffff:ffff:ffff:ffff:e000::/67",
+                    "ffff:ffff:ffff:ffff:f000::/68",
+                    "ffff:ffff:ffff:ffff:f800::/69",
+                    "ffff:ffff:ffff:ffff:fc00::/70",
+                    "ffff:ffff:ffff:ffff:fe00::/71",
+                    "ffff:ffff:ffff:ffff:ff00::/72",
+                    "ffff:ffff:ffff:ffff:ff80::/73",
+                    "ffff:ffff:ffff:ffff:ffc0::/74",
+                    "ffff:ffff:ffff:ffff:ffe0::/75",
+                    "ffff:ffff:ffff:ffff:fff0::/76",
+                    "ffff:ffff:ffff:ffff:fff8::/77",
+                    "ffff:ffff:ffff:ffff:fffc::/78",
+                    "ffff:ffff:ffff:ffff:fffe::/79",
+                    "ffff:ffff:ffff:ffff:ffff::/80",
+                    "ffff:ffff:ffff:ffff:ffff:8000::/81",
+                    "ffff:ffff:ffff:ffff:ffff:c000::/82",
+                    "ffff:ffff:ffff:ffff:ffff:e000::/83",
+                    "ffff:ffff:ffff:ffff:ffff:f000::/84",
+                    "ffff:ffff:ffff:ffff:ffff:f800::/85",
+                    "ffff:ffff:ffff:ffff:ffff:fc00::/86",
+                    "ffff:ffff:ffff:ffff:ffff:fe00::/87",
+                    "ffff:ffff:ffff:ffff:ffff:ff00::/88",
+                    "ffff:ffff:ffff:ffff:ffff:ff80::/89",
+                    "ffff:ffff:ffff:ffff:ffff:ffc0::/90",
+                    "ffff:ffff:ffff:ffff:ffff:ffe0::/91",
+                    "ffff:ffff:ffff:ffff:ffff:fff0::/92",
+                    "ffff:ffff:ffff:ffff:ffff:fff8::/93",
+                    "ffff:ffff:ffff:ffff:ffff:fffc::/94",
+                    "ffff:ffff:ffff:ffff:ffff:fffe::/95",
+                    "ffff:ffff:ffff:ffff:ffff:ffff::/96",
+                    // Note: The result of /97 to /112 does not meet RFC requirements:
+                    // 4.2.2.  Handling One 16-Bit 0 Field
+                    // The symbol "::" MUST NOT be used to shorten just one 16-bit 0 field.
+                    "ffff:ffff:ffff:ffff:ffff:ffff:8000::/97",
+                    "ffff:ffff:ffff:ffff:ffff:ffff:c000::/98",
+                    "ffff:ffff:ffff:ffff:ffff:ffff:e000::/99",
+                    "ffff:ffff:ffff:ffff:ffff:ffff:f000::/100",
+                    "ffff:ffff:ffff:ffff:ffff:ffff:f800::/101",
+                    "ffff:ffff:ffff:ffff:ffff:ffff:fc00::/102",
+                    "ffff:ffff:ffff:ffff:ffff:ffff:fe00::/103",
+                    "ffff:ffff:ffff:ffff:ffff:ffff:ff00::/104",
+                    "ffff:ffff:ffff:ffff:ffff:ffff:ff80::/105",
+                    "ffff:ffff:ffff:ffff:ffff:ffff:ffc0::/106",
+                    "ffff:ffff:ffff:ffff:ffff:ffff:ffe0::/107",
+                    "ffff:ffff:ffff:ffff:ffff:ffff:fff0::/108",
+                    "ffff:ffff:ffff:ffff:ffff:ffff:fff8::/109",
+                    "ffff:ffff:ffff:ffff:ffff:ffff:fffc::/110",
+                    "ffff:ffff:ffff:ffff:ffff:ffff:fffe::/111",
+                    "ffff:ffff:ffff:ffff:ffff:ffff:ffff::/112",
+                    "ffff:ffff:ffff:ffff:ffff:ffff:ffff:8000/113",
+                    "ffff:ffff:ffff:ffff:ffff:ffff:ffff:c000/114",
+                    "ffff:ffff:ffff:ffff:ffff:ffff:ffff:e000/115",
+                    "ffff:ffff:ffff:ffff:ffff:ffff:ffff:f000/116",
+                    "ffff:ffff:ffff:ffff:ffff:ffff:ffff:f800/117",
+                    "ffff:ffff:ffff:ffff:ffff:ffff:ffff:fc00/118",
+                    "ffff:ffff:ffff:ffff:ffff:ffff:ffff:fe00/119",
+                    "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ff00/120",
+                    "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ff80/121",
+                    "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffc0/122",
+                    "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffe0/123",
+                    "ffff:ffff:ffff:ffff:ffff:ffff:ffff:fff0/124",
+                    "ffff:ffff:ffff:ffff:ffff:ffff:ffff:fff8/125",
+                    "ffff:ffff:ffff:ffff:ffff:ffff:ffff:fffc/126",
+                    "ffff:ffff:ffff:ffff:ffff:ffff:ffff:fffe/127",
+                    "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff/128",
+                },
+        },
+    };
+
+    printf("Tidy Prefixes:\n");
+
+    for (auto test : kPrefixes)
+    {
+        for (uint16_t i = 0; i < ot::GetArrayLength(test.prefixStringAfterTidy); i++)
+        {
+            ot::Ip6::Prefix prefix, answer;
+
+            SuccessOrQuit(answer.FromString(test.prefixStringAfterTidy[i]));
+            prefix.Set(test.originalPrefix, i);
+            prefix.Tidy();
+
+            {
+                ot::Ip6::Prefix::InfoString prefixString = prefix.ToString();
+
+                printf("Prefix: %-36s  TidyResult: %-36s\n", test.prefixStringAfterTidy[i],
+                       prefix.ToString().AsCString());
+
+                VerifyOrQuit(memcmp(answer.mPrefix.mFields.m8, prefix.mPrefix.mFields.m8,
+                                    sizeof(answer.mPrefix.mFields.m8)) == 0);
+                VerifyOrQuit(prefix.mLength == answer.mLength);
+                VerifyOrQuit(strcmp(test.prefixStringAfterTidy[i], prefixString.AsCString()) == 0);
+            }
+        }
+    }
+}
+
 void TestIp4Ip6Translation(void)
 {
     struct TestCase
@@ -580,9 +880,12 @@
     TestIp6AddressSetPrefix();
     TestIp4AddressFromString();
     TestIp6AddressFromString();
+    TestIp6PrefixFromString();
     TestIp6Prefix();
+    TestIp6PrefixTidy();
     TestIp4Ip6Translation();
     TestIp4Cidr();
+    TestIp4CidrFromString();
     printf("All tests passed\n");
     return 0;
 }
diff --git a/tests/unit/test_link_quality.cpp b/tests/unit/test_link_quality.cpp
index a1155ef..2562ec8 100644
--- a/tests/unit/test_link_quality.cpp
+++ b/tests/unit/test_link_quality.cpp
@@ -57,7 +57,7 @@
 #define MAX_RSS(_rss1, _rss2) (((_rss1) < (_rss2)) ? (_rss2) : (_rss1))
 #define ABS(value) (((value) >= 0) ? (value) : -(value))
 
-// This struct contains RSS values and test data for checking link quality info calss.
+// This struct contains RSS values and test data for checking link quality info class.
 struct RssTestData
 {
     const int8_t *mRssList;             // Array of RSS values.
@@ -494,13 +494,13 @@
 
             printf("\nLinkMargin : %-3u -> Scaled : %.1f (rounded:%u)", linkMargin, scaled, scaledAsU8);
 
-            VerifyOrQuit(LinkMetrics::LinkMetrics::ScaleLinkMarginToRawValue(linkMargin) == scaledAsU8);
-            VerifyOrQuit(LinkMetrics::LinkMetrics::ScaleRawValueToLinkMargin(scaledAsU8) == linkMargin);
+            VerifyOrQuit(LinkMetrics::ScaleLinkMarginToRawValue(linkMargin) == scaledAsU8);
+            VerifyOrQuit(LinkMetrics::ScaleRawValueToLinkMargin(scaledAsU8) == linkMargin);
         }
 
-        VerifyOrQuit(LinkMetrics::LinkMetrics::ScaleLinkMarginToRawValue(131) == 255);
-        VerifyOrQuit(LinkMetrics::LinkMetrics::ScaleLinkMarginToRawValue(150) == 255);
-        VerifyOrQuit(LinkMetrics::LinkMetrics::ScaleLinkMarginToRawValue(255) == 255);
+        VerifyOrQuit(LinkMetrics::ScaleLinkMarginToRawValue(131) == 255);
+        VerifyOrQuit(LinkMetrics::ScaleLinkMarginToRawValue(150) == 255);
+        VerifyOrQuit(LinkMetrics::ScaleLinkMarginToRawValue(255) == 255);
 
         // Test RSSI scaling from [-130, 0] -> [0, 255]
 
@@ -511,13 +511,13 @@
 
             printf("\nRSSI : %-3d -> Scaled :%.1f (rounded:%u)", rssi, scaled, scaledAsU8);
 
-            VerifyOrQuit(LinkMetrics::LinkMetrics::ScaleRssiToRawValue(rssi) == scaledAsU8);
-            VerifyOrQuit(LinkMetrics::LinkMetrics::ScaleRawValueToRssi(scaledAsU8) == rssi);
+            VerifyOrQuit(LinkMetrics::ScaleRssiToRawValue(rssi) == scaledAsU8);
+            VerifyOrQuit(LinkMetrics::ScaleRawValueToRssi(scaledAsU8) == rssi);
         }
 
-        VerifyOrQuit(LinkMetrics::LinkMetrics::ScaleRssiToRawValue(1) == 255);
-        VerifyOrQuit(LinkMetrics::LinkMetrics::ScaleRssiToRawValue(10) == 255);
-        VerifyOrQuit(LinkMetrics::LinkMetrics::ScaleRssiToRawValue(127) == 255);
+        VerifyOrQuit(LinkMetrics::ScaleRssiToRawValue(1) == 255);
+        VerifyOrQuit(LinkMetrics::ScaleRssiToRawValue(10) == 255);
+        VerifyOrQuit(LinkMetrics::ScaleRssiToRawValue(127) == 255);
     }
 };
 
diff --git a/tests/unit/test_lowpan.cpp b/tests/unit/test_lowpan.cpp
index c28f8fd..f50f868 100644
--- a/tests/unit/test_lowpan.cpp
+++ b/tests/unit/test_lowpan.cpp
@@ -277,7 +277,7 @@
 
 static void TestFullyCompressableLongAddresses(void)
 {
-    TestIphcVector testVector("Fully compressable IPv6 addresses using long MAC addresses");
+    TestIphcVector testVector("Fully compressible IPv6 addresses using long MAC addresses");
 
     // Setup MAC addresses.
     testVector.SetMacSource(sTestMacSourceDefaultLong);
@@ -302,7 +302,7 @@
 
 static void TestFullyCompressableShortAddresses(void)
 {
-    TestIphcVector testVector("Fully compressable IPv6 addresses using short MAC addresses");
+    TestIphcVector testVector("Fully compressible IPv6 addresses using short MAC addresses");
 
     // Setup MAC addresses.
     testVector.SetMacSource(sTestMacSourceDefaultShort);
@@ -327,7 +327,7 @@
 
 static void TestFullyCompressableShortLongAddresses(void)
 {
-    TestIphcVector testVector("Fully compressable IPv6 addresses using short and long MAC addresses");
+    TestIphcVector testVector("Fully compressible IPv6 addresses using short and long MAC addresses");
 
     // Setup MAC addresses.
     testVector.SetMacSource(sTestMacSourceDefaultShort);
@@ -352,7 +352,7 @@
 
 static void TestFullyCompressableLongShortAddresses(void)
 {
-    TestIphcVector testVector("Fully compressable IPv6 addresses using long and short MAC addresses");
+    TestIphcVector testVector("Fully compressible IPv6 addresses using long and short MAC addresses");
 
     // Setup MAC addresses.
     testVector.SetMacSource(sTestMacSourceDefaultLong);
@@ -505,7 +505,7 @@
 
 static void TestSourceCompressedDestination16bitAddresses(void)
 {
-    TestIphcVector testVector("Fully compressable IPv6 source and destination 16-bit using long MAC addresses");
+    TestIphcVector testVector("Fully compressible IPv6 source and destination 16-bit using long MAC addresses");
 
     // Setup MAC addresses.
     testVector.SetMacSource(sTestMacSourceDefaultLong);
@@ -530,7 +530,7 @@
 
 static void TestSourceCompressedDestination128bitAddresses(void)
 {
-    TestIphcVector testVector("Fully compressable IPv6 source and destination inline using long MAC addresses");
+    TestIphcVector testVector("Fully compressible IPv6 source and destination inline using long MAC addresses");
 
     // Setup MAC addresses.
     testVector.SetMacSource(sTestMacSourceDefaultLong);
@@ -734,7 +734,7 @@
 
 static void TestStatefulCompressableLongAddressesContext0(void)
 {
-    TestIphcVector testVector("Stateful compression compressable long addresses, context 0");
+    TestIphcVector testVector("Stateful compression compressible long addresses, context 0");
 
     // Setup MAC addresses.
     testVector.SetMacSource(sTestMacSourceDefaultLong);
@@ -759,7 +759,7 @@
 
 static void TestStatefulCompressableShortAddressesContext0(void)
 {
-    TestIphcVector testVector("Stateful compression compressable short addresses, context 0");
+    TestIphcVector testVector("Stateful compression compressible short addresses, context 0");
 
     // Setup MAC addresses.
     testVector.SetMacSource(sTestMacSourceDefaultShort);
@@ -784,7 +784,7 @@
 
 static void TestStatefulCompressableLongShortAddressesContext0(void)
 {
-    TestIphcVector testVector("Stateful compression compressable long and short addresses, context 0");
+    TestIphcVector testVector("Stateful compression compressible long and short addresses, context 0");
 
     // Setup MAC addresses.
     testVector.SetMacSource(sTestMacSourceDefaultLong);
diff --git a/tests/unit/test_platform.cpp b/tests/unit/test_platform.cpp
index e2ef719..5487643 100644
--- a/tests/unit/test_platform.cpp
+++ b/tests/unit/test_platform.cpp
@@ -26,8 +26,13 @@
  *  POSSIBILITY OF SUCH DAMAGE.
  */
 
+// Disable OpenThread's own new implementation to avoid duplicate definition
+#define OT_CORE_COMMON_NEW_HPP_
 #include "test_platform.h"
 
+#include <map>
+#include <vector>
+
 #include <stdio.h>
 #include <sys/time.h>
 
@@ -37,6 +42,8 @@
     FLASH_SWAP_NUM  = 2,
 };
 
+std::map<uint32_t, std::vector<std::vector<uint8_t>>> settings;
+
 ot::Instance *testInitInstance(void)
 {
     otInstance *instance = nullptr;
@@ -224,18 +231,78 @@
 
 OT_TOOL_WEAK void otPlatSettingsDeinit(otInstance *) {}
 
-OT_TOOL_WEAK otError otPlatSettingsGet(otInstance *, uint16_t, int, uint8_t *, uint16_t *)
+OT_TOOL_WEAK otError otPlatSettingsGet(otInstance *, uint16_t aKey, int aIndex, uint8_t *aValue, uint16_t *aValueLength)
 {
-    return OT_ERROR_NOT_FOUND;
+    auto setting = settings.find(aKey);
+
+    if (setting == settings.end())
+    {
+        return OT_ERROR_NOT_FOUND;
+    }
+
+    if (aIndex > setting->second.size())
+    {
+        return OT_ERROR_NOT_FOUND;
+    }
+
+    if (aValueLength == nullptr)
+    {
+        return OT_ERROR_NONE;
+    }
+
+    const auto &data = setting->second[aIndex];
+
+    if (aValue == nullptr)
+    {
+        *aValueLength = data.size();
+        return OT_ERROR_NONE;
+    }
+
+    if (*aValueLength >= data.size())
+    {
+        *aValueLength = data.size();
+    }
+
+    memcpy(aValue, &data[0], *aValueLength);
+
+    return OT_ERROR_NONE;
 }
 
-OT_TOOL_WEAK otError otPlatSettingsSet(otInstance *, uint16_t, const uint8_t *, uint16_t) { return OT_ERROR_NONE; }
+OT_TOOL_WEAK otError otPlatSettingsSet(otInstance *, uint16_t aKey, const uint8_t *aValue, uint16_t aValueLength)
+{
+    auto setting = std::vector<uint8_t>(aValue, aValue + aValueLength);
 
-OT_TOOL_WEAK otError otPlatSettingsAdd(otInstance *, uint16_t, const uint8_t *, uint16_t) { return OT_ERROR_NONE; }
+    settings[aKey].clear();
+    settings[aKey].push_back(setting);
 
-OT_TOOL_WEAK otError otPlatSettingsDelete(otInstance *, uint16_t, int) { return OT_ERROR_NONE; }
+    return OT_ERROR_NONE;
+}
 
-OT_TOOL_WEAK void otPlatSettingsWipe(otInstance *) {}
+OT_TOOL_WEAK otError otPlatSettingsAdd(otInstance *, uint16_t aKey, const uint8_t *aValue, uint16_t aValueLength)
+{
+    auto setting = std::vector<uint8_t>(aValue, aValue + aValueLength);
+    settings[aKey].push_back(setting);
+
+    return OT_ERROR_NONE;
+}
+
+OT_TOOL_WEAK otError otPlatSettingsDelete(otInstance *, uint16_t aKey, int aIndex)
+{
+    auto setting = settings.find(aKey);
+    if (setting == settings.end())
+    {
+        return OT_ERROR_NOT_FOUND;
+    }
+
+    if (aIndex >= setting->second.size())
+    {
+        return OT_ERROR_NOT_FOUND;
+    }
+    setting->second.erase(setting->second.begin() + aIndex);
+    return OT_ERROR_NONE;
+}
+
+OT_TOOL_WEAK void otPlatSettingsWipe(otInstance *) { settings.clear(); }
 
 uint8_t *GetFlash(void)
 {
@@ -409,6 +476,43 @@
 
 #endif // OPENTHREAD_CONFIG_PLATFORM_KEY_REFERENCES_ENABLE
 
+otError otPlatCryptoEcdsaGenerateAndImportKey(otCryptoKeyRef aKeyRef)
+{
+    OT_UNUSED_VARIABLE(aKeyRef);
+
+    return OT_ERROR_NONE;
+}
+
+otError otPlatCryptoEcdsaExportPublicKey(otCryptoKeyRef aKeyRef, otPlatCryptoEcdsaPublicKey *aPublicKey)
+{
+    OT_UNUSED_VARIABLE(aKeyRef);
+    OT_UNUSED_VARIABLE(aPublicKey);
+
+    return OT_ERROR_NONE;
+}
+
+otError otPlatCryptoEcdsaSignUsingKeyRef(otCryptoKeyRef                aKeyRef,
+                                         const otPlatCryptoSha256Hash *aHash,
+                                         otPlatCryptoEcdsaSignature   *aSignature)
+{
+    OT_UNUSED_VARIABLE(aKeyRef);
+    OT_UNUSED_VARIABLE(aHash);
+    OT_UNUSED_VARIABLE(aSignature);
+
+    return OT_ERROR_NONE;
+}
+
+otError otPlatCryptoEcdsaVerifyUsingKeyRef(otCryptoKeyRef                    aKeyRef,
+                                           const otPlatCryptoSha256Hash     *aHash,
+                                           const otPlatCryptoEcdsaSignature *aSignature)
+{
+    OT_UNUSED_VARIABLE(aKeyRef);
+    OT_UNUSED_VARIABLE(aHash);
+    OT_UNUSED_VARIABLE(aSignature);
+
+    return OT_ERROR_NONE;
+}
+
 otError otPlatRadioSetCcaEnergyDetectThreshold(otInstance *aInstance, int8_t aThreshold)
 {
     OT_UNUSED_VARIABLE(aInstance);
diff --git a/tests/unit/test_routing_manager.cpp b/tests/unit/test_routing_manager.cpp
index a463c13..93246fa 100644
--- a/tests/unit/test_routing_manager.cpp
+++ b/tests/unit/test_routing_manager.cpp
@@ -158,6 +158,7 @@
 const char *PreferenceToString(int8_t aPreference);
 void        SendRouterAdvert(const Ip6::Address &aAddress, const Icmp6Packet &aPacket);
 void        SendNeighborAdvert(const Ip6::Address &aAddress, const Ip6::Nd::NeighborAdvertMessage &aNaMessage);
+void        DiscoverNat64Prefix(const Ip6::Prefix &aPrefix);
 
 #if OPENTHREAD_CONFIG_LOG_OUTPUT == OPENTHREAD_CONFIG_LOG_OUTPUT_PLATFORM_DEFINED
 void otPlatLog(otLogLevel aLogLevel, otLogRegion aLogRegion, const char *aFormat, ...)
@@ -502,6 +503,13 @@
                              sizeof(aNaMessage));
 }
 
+void DiscoverNat64Prefix(const Ip6::Prefix &aPrefix)
+{
+    Log("Discovered NAT64 prefix %s", aPrefix.ToString().AsCString());
+
+    otPlatInfraIfDiscoverNat64PrefixDone(sInstance, kInfraIfIndex, &aPrefix);
+}
+
 Ip6::Prefix PrefixFromString(const char *aString, uint8_t aPrefixLength)
 {
     Ip6::Prefix prefix;
@@ -521,7 +529,7 @@
     return address;
 }
 
-void VerifyOmrPrefixInNetData(const Ip6::Prefix &aOmrPrefix, bool aDefaultRoute = false)
+void VerifyOmrPrefixInNetData(const Ip6::Prefix &aOmrPrefix, bool aDefaultRoute)
 {
     otNetworkDataIterator           iterator = OT_NETWORK_DATA_ITERATOR_INIT;
     NetworkData::OnMeshPrefixConfig prefixConfig;
@@ -550,60 +558,68 @@
 
 using NetworkData::RoutePreference;
 
-struct ExternalRoute
+enum ExternalRouteMode : uint8_t
 {
-    ExternalRoute(const Ip6::Prefix &aPrefix, RoutePreference aPreference)
-        : mPrefix(aPrefix)
-        , mPreference(aPreference)
-    {
-    }
-
-    const Ip6::Prefix &mPrefix;
-    RoutePreference    mPreference;
+    kNoRoute,
+    kDefaultRoute,
+    kUlaRoute,
 };
 
-template <uint16_t kLength> void VerifyExternalRoutesInNetData(const ExternalRoute (&aExternRoutes)[kLength])
+void VerifyExternalRouteInNetData(ExternalRouteMode aMode)
+{
+    Error                 error;
+    otNetworkDataIterator iterator = OT_NETWORK_DATA_ITERATOR_INIT;
+    otExternalRouteConfig routeConfig;
+
+    error = otNetDataGetNextRoute(sInstance, &iterator, &routeConfig);
+
+    switch (aMode)
+    {
+    case kNoRoute:
+        Log("VerifyExternalRouteInNetData(kNoRoute)");
+        VerifyOrQuit(error != kErrorNone);
+        break;
+
+    case kDefaultRoute:
+        Log("VerifyExternalRouteInNetData(kDefaultRoute)");
+        VerifyOrQuit(error == kErrorNone);
+        VerifyOrQuit(routeConfig.mPrefix.mLength == 0);
+        VerifyOrQuit(otNetDataGetNextRoute(sInstance, &iterator, &routeConfig) != kErrorNone);
+        break;
+
+    case kUlaRoute:
+        Log("VerifyExternalRouteInNetData(kUlaRoute)");
+        VerifyOrQuit(error == kErrorNone);
+        VerifyOrQuit(routeConfig.mPrefix.mLength == 7);
+        VerifyOrQuit(routeConfig.mPrefix.mPrefix.mFields.m8[0] == 0xfc);
+        VerifyOrQuit(otNetDataGetNextRoute(sInstance, &iterator, &routeConfig) != kErrorNone);
+        break;
+    }
+}
+
+void VerifyNat64PrefixInNetData(const Ip6::Prefix &aNat64Prefix)
 {
     otNetworkDataIterator            iterator = OT_NETWORK_DATA_ITERATOR_INIT;
     NetworkData::ExternalRouteConfig routeConfig;
-    uint16_t                         counter;
+    bool                             didFind = false;
 
-    Log("VerifyExternalRoutesInNetData()");
-
-    counter = 0;
+    Log("VerifyNat64PrefixInNetData()");
 
     while (otNetDataGetNextRoute(sInstance, &iterator, &routeConfig) == kErrorNone)
     {
-        bool didFind = false;
-
-        counter++;
-
-        Log("   prefix:%s, prf:%s", routeConfig.GetPrefix().ToString().AsCString(),
-            PreferenceToString(routeConfig.mPreference));
-
-        for (const ExternalRoute &externalRoute : aExternRoutes)
+        if (!routeConfig.mNat64 || !routeConfig.GetPrefix().IsValidNat64())
         {
-            if (externalRoute.mPrefix == routeConfig.GetPrefix())
-            {
-                VerifyOrQuit(static_cast<int8_t>(routeConfig.mPreference) == externalRoute.mPreference);
-                didFind = true;
-                break;
-            }
+            continue;
         }
 
-        VerifyOrQuit(didFind);
+        Log("   nat64 prefix:%s, prf:%s", routeConfig.GetPrefix().ToString().AsCString(),
+            PreferenceToString(routeConfig.mPreference));
+
+        VerifyOrQuit(routeConfig.GetPrefix() == aNat64Prefix);
+        didFind = true;
     }
 
-    VerifyOrQuit(counter == kLength);
-}
-
-void VerifyNoExternalRouteInNetData(void)
-{
-    otNetworkDataIterator            iterator = OT_NETWORK_DATA_ITERATOR_INIT;
-    NetworkData::ExternalRouteConfig routeConfig;
-
-    Log("VerifyNoExternalRouteInNetData()");
-    VerifyOrQuit(otNetDataGetNextRoute(sInstance, &iterator, &routeConfig) != kErrorNone);
+    VerifyOrQuit(didFind);
 }
 
 struct Pio
@@ -749,6 +765,11 @@
     VerifyPrefixTable(aOnLinkPrefixes, kNumOnLinkPrefixes, nullptr, 0);
 }
 
+template <uint16_t kNumRoutePrefixes> void VerifyPrefixTable(const RoutePrefix (&aRoutePrefixes)[kNumRoutePrefixes])
+{
+    VerifyPrefixTable(nullptr, 0, aRoutePrefixes, kNumRoutePrefixes);
+}
+
 void VerifyPrefixTable(const OnLinkPrefix *aOnLinkPrefixes,
                        uint16_t            aNumOnLinkPrefixes,
                        const RoutePrefix  *aRoutePrefixes,
@@ -820,6 +841,8 @@
     VerifyOrQuit(routePrefixCount == aNumRoutePrefixes);
 }
 
+void VerifyPrefixTableIsEmpty(void) { VerifyPrefixTable(nullptr, 0, nullptr, 0); }
+
 void InitTest(bool aEnablBorderRouting = false, bool aAfterReset = false)
 {
     //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
@@ -916,8 +939,8 @@
     //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
     // Check Network Data to include the local OMR and on-link prefix.
 
-    VerifyOmrPrefixInNetData(localOmr);
-    VerifyExternalRoutesInNetData({ExternalRoute(localOnLink, NetworkData::kRoutePreferenceMedium)});
+    VerifyOmrPrefixInNetData(localOmr, /* aDefaultRoute */ false);
+    VerifyExternalRouteInNetData(kUlaRoute);
 
     //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
     // Send an RA from router A with a new on-link (PIO) and route prefix (RIO).
@@ -944,10 +967,8 @@
     //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
     // Check Network Data to include new prefixes from router A.
 
-    VerifyOmrPrefixInNetData(localOmr);
-    VerifyExternalRoutesInNetData({ExternalRoute(localOnLink, NetworkData::kRoutePreferenceMedium),
-                                   ExternalRoute(onLinkPrefix, NetworkData::kRoutePreferenceMedium),
-                                   ExternalRoute(routePrefix, NetworkData::kRoutePreferenceMedium)});
+    VerifyOmrPrefixInNetData(localOmr, /* aDefaultRoute */ true);
+    VerifyExternalRouteInNetData(kDefaultRoute);
 
     //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
     // Send the same RA again from router A with the on-link (PIO) and route prefix (RIO).
@@ -981,15 +1002,8 @@
     //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
     // Check Network Data.
 
-    VerifyOmrPrefixInNetData(localOmr);
-
-    // We expect to see 3 entries, our local on link and new prefixes
-    // from router A and B. The `routePrefix` now should have high
-    // preference.
-
-    VerifyExternalRoutesInNetData({ExternalRoute(routePrefix, NetworkData::kRoutePreferenceHigh),
-                                   ExternalRoute(localOnLink, NetworkData::kRoutePreferenceMedium),
-                                   ExternalRoute(onLinkPrefix, NetworkData::kRoutePreferenceMedium)});
+    VerifyOmrPrefixInNetData(localOmr, /* aDefaultRoute */ true);
+    VerifyExternalRouteInNetData(kDefaultRoute);
 
     //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
     // Send an RA from router B removing the route prefix.
@@ -1006,11 +1020,9 @@
                       {RoutePrefix(routePrefix, kValidLitime, NetworkData::kRoutePreferenceMedium, routerAddressA)});
 
     //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-    // Check Network Data, all prefixes should be again at medium preference.
+    // Check Network Data.
 
-    VerifyExternalRoutesInNetData({ExternalRoute(localOnLink, NetworkData::kRoutePreferenceMedium),
-                                   ExternalRoute(onLinkPrefix, NetworkData::kRoutePreferenceMedium),
-                                   ExternalRoute(routePrefix, NetworkData::kRoutePreferenceMedium)});
+    VerifyExternalRouteInNetData(kDefaultRoute);
 
     //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 
@@ -1059,8 +1071,8 @@
     //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
     // Check Network Data to include the local OMR and on-link prefix.
 
-    VerifyOmrPrefixInNetData(localOmr);
-    VerifyExternalRoutesInNetData({ExternalRoute(localOnLink, NetworkData::kRoutePreferenceMedium)});
+    VerifyOmrPrefixInNetData(localOmr, /* aDefaultRoute */ false);
+    VerifyExternalRouteInNetData(kUlaRoute);
 
     //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
     // Add a new OMR prefix directly into net data. The new prefix should
@@ -1097,9 +1109,8 @@
     // Check Network Data. We should now see that the local OMR prefix
     // is removed.
 
-    VerifyOmrPrefixInNetData(omrPrefix);
-
-    VerifyExternalRoutesInNetData({ExternalRoute(localOnLink, NetworkData::kRoutePreferenceMedium)});
+    VerifyOmrPrefixInNetData(omrPrefix, /* aDefaultRoute */ false);
+    VerifyExternalRouteInNetData(kUlaRoute);
 
     //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
     // Remove the OMR prefix previously added in net data.
@@ -1125,9 +1136,8 @@
     // Check Network Data. We should see that the local OMR prefix is
     // added again.
 
-    VerifyOmrPrefixInNetData(localOmr);
-
-    VerifyExternalRoutesInNetData({ExternalRoute(localOnLink, NetworkData::kRoutePreferenceMedium)});
+    VerifyOmrPrefixInNetData(localOmr, /* aDefaultRoute */ false);
+    VerifyExternalRouteInNetData(kUlaRoute);
 
     //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 
@@ -1139,12 +1149,9 @@
 {
     Ip6::Prefix                     localOnLink;
     Ip6::Prefix                     localOmr;
-    Ip6::Prefix                     onLinkPrefix   = PrefixFromString("2000:abba:baba::", 64);
-    Ip6::Prefix                     routePrefix    = PrefixFromString("2000:1234:5678::", 64);
     Ip6::Prefix                     omrPrefix      = PrefixFromString("2000:0000:1111:4444::", 64);
     Ip6::Prefix                     defaultRoute   = PrefixFromString("::", 0);
     Ip6::Address                    routerAddressA = AddressFromString("fd00::aaaa");
-    Ip6::Address                    routerAddressB = AddressFromString("fd00::bbbb");
     NetworkData::OnMeshPrefixConfig prefixConfig;
 
     Log("--------------------------------------------------------------------------------------------");
@@ -1153,53 +1160,60 @@
     InitTest();
 
     //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-    // Start Routing Manager
+    // Start Routing Manager. Check emitted RS and RA messages.
+
+    sRsEmitted   = false;
+    sRaValidated = false;
+    sExpectedPio = kPioAdvertisingLocalOnLink;
+    sExpectedRios.Clear();
 
     SuccessOrQuit(sInstance->Get<BorderRouter::RoutingManager>().SetEnabled(true));
 
     SuccessOrQuit(sInstance->Get<BorderRouter::RoutingManager>().GetOnLinkPrefix(localOnLink));
     SuccessOrQuit(sInstance->Get<BorderRouter::RoutingManager>().GetOmrPrefix(localOmr));
 
-    AdvanceTime(500);
-
     Log("Local on-link prefix is %s", localOnLink.ToString().AsCString());
     Log("Local OMR prefix is %s", localOmr.ToString().AsCString());
 
+    sExpectedRios.Add(localOmr);
+
+    AdvanceTime(30000);
+
+    VerifyOrQuit(sRsEmitted);
+    VerifyOrQuit(sRaValidated);
+    VerifyOrQuit(sExpectedRios.SawAll());
+    Log("Received RA was validated");
+
     //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-    // Send an RA from router A and B adding an onlink prefix
-    // and routePrefix from A, and a default route from B.
+    // Check Network Data to include the local OMR and ULA prefix.
 
-    SendRouterAdvert(routerAddressA, {Pio(onLinkPrefix, kValidLitime, kPreferredLifetime)},
-                     {Rio(routePrefix, kValidLitime, NetworkData::kRoutePreferenceMedium)});
+    VerifyOmrPrefixInNetData(localOmr, /* aDefaultRoute */ false);
+    VerifyExternalRouteInNetData(kUlaRoute);
 
-    SendRouterAdvert(routerAddressB, DefaultRoute(kValidLitime, NetworkData::kRoutePreferenceLow));
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Send RA from router A advertising a default route.
 
-    sRsEmitted   = false;
-    sRaValidated = false;
-    sExpectedPio = kNoPio;
-    sExpectedRios.Clear();
+    SendRouterAdvert(routerAddressA, DefaultRoute(kValidLitime, NetworkData::kRoutePreferenceLow));
 
     AdvanceTime(10000);
 
     //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-    // Check the discovered prefix table and ensure info from router B
-    // is now included in the table.
+    // Check the discovered prefix table and ensure default route
+    // from router A is in the table.
 
-    VerifyPrefixTable({OnLinkPrefix(onLinkPrefix, kValidLitime, kPreferredLifetime, routerAddressA)},
-                      {RoutePrefix(routePrefix, kValidLitime, NetworkData::kRoutePreferenceMedium, routerAddressA),
-                       RoutePrefix(defaultRoute, kValidLitime, NetworkData::kRoutePreferenceLow, routerAddressB)});
+    VerifyPrefixTable({RoutePrefix(defaultRoute, kValidLitime, NetworkData::kRoutePreferenceLow, routerAddressA)});
 
     //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
     // Check Network Data. We should not see default route in
-    // Network Data yet (since there is no OMR prefix with default
-    // route flag).
+    // Network Data yet since there is no infrastructure-derived
+    // OMR prefix (with preference medium or higher).
 
-    VerifyExternalRoutesInNetData({ExternalRoute(onLinkPrefix, NetworkData::kRoutePreferenceMedium),
-                                   ExternalRoute(routePrefix, NetworkData::kRoutePreferenceMedium)});
+    VerifyOmrPrefixInNetData(localOmr, /* aDefaultRoute */ false);
+    VerifyExternalRouteInNetData(kUlaRoute);
 
     //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-    // Add an OMR prefix directly into Network Data with default route
-    // flag.
+    // Add an OMR prefix directly into Network Data with
+    // preference medium (infrastructure-derived).
 
     prefixConfig.Clear();
     prefixConfig.mPrefix       = omrPrefix;
@@ -1216,42 +1230,14 @@
     AdvanceTime(10000);
 
     //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-    // Check Network Data. We should now see default route from
-    // router B.
+    // Check Network Data. Now that we have an infrastructure-derived
+    // OMR prefix, the default route should be published.
 
-    VerifyExternalRoutesInNetData({ExternalRoute(onLinkPrefix, NetworkData::kRoutePreferenceMedium),
-                                   ExternalRoute(routePrefix, NetworkData::kRoutePreferenceMedium),
-                                   ExternalRoute(defaultRoute, NetworkData::kRoutePreferenceLow)});
+    VerifyOmrPrefixInNetData(omrPrefix, /* aDefaultRoute */ true);
+    VerifyExternalRouteInNetData(kDefaultRoute);
 
     //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-    // Send an RA from router B adding a default route
-    // now also as ::/0 prefix RIO with a high preference.
-
-    SendRouterAdvert(routerAddressB, {Rio(defaultRoute, kValidLitime, NetworkData::kRoutePreferenceHigh)},
-                     DefaultRoute(kValidLitime, NetworkData::kRoutePreferenceLow));
-
-    AdvanceTime(10000);
-
-    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-    // Check the discovered prefix table and ensure default route
-    // entry from router B is now correctly updated to use high
-    // preference (RIO overrides the default route info from header).
-
-    VerifyPrefixTable({OnLinkPrefix(onLinkPrefix, kValidLitime, kPreferredLifetime, routerAddressA)},
-                      {RoutePrefix(routePrefix, kValidLitime, NetworkData::kRoutePreferenceMedium, routerAddressA),
-                       RoutePrefix(defaultRoute, kValidLitime, NetworkData::kRoutePreferenceHigh, routerAddressB)});
-
-    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-    // Check Network Data. We should now see default route from
-    // router B included with high preference.
-
-    VerifyExternalRoutesInNetData({ExternalRoute(onLinkPrefix, NetworkData::kRoutePreferenceMedium),
-                                   ExternalRoute(routePrefix, NetworkData::kRoutePreferenceMedium),
-                                   ExternalRoute(defaultRoute, NetworkData::kRoutePreferenceHigh)});
-
-    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-    // Remove the OMR prefix previously added with default route
-    // flag.
+    // Remove the OMR prefix from Network Data.
 
     SuccessOrQuit(otBorderRouterRemoveOnMeshPrefix(sInstance, &omrPrefix));
     SuccessOrQuit(otBorderRouterRegister(sInstance));
@@ -1259,14 +1245,136 @@
     AdvanceTime(10000);
 
     //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-    // Check Network Data. The default route ::/0 should be now
-    // removed since the OMR prefix with default route is removed.
+    // Check Network Data. We should again go back to ULA prefix. The
+    // default route advertised by router A should be still present in
+    // the discovered prefix table.
 
-    VerifyExternalRoutesInNetData({ExternalRoute(onLinkPrefix, NetworkData::kRoutePreferenceMedium),
-                                   ExternalRoute(routePrefix, NetworkData::kRoutePreferenceMedium)});
+    VerifyOmrPrefixInNetData(localOmr, /* aDefaultRoute */ false);
+    VerifyExternalRouteInNetData(kUlaRoute);
+
+    VerifyPrefixTable({RoutePrefix(defaultRoute, kValidLitime, NetworkData::kRoutePreferenceLow, routerAddressA)});
 
     //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-    // Re-add the OMR prefix with default route flag.
+    // Add the OMR prefix again.
+
+    SuccessOrQuit(otBorderRouterAddOnMeshPrefix(sInstance, &prefixConfig));
+    SuccessOrQuit(otBorderRouterRegister(sInstance));
+
+    AdvanceTime(10000);
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Check Network Data. Again the default route should be published.
+
+    VerifyOmrPrefixInNetData(omrPrefix, /* aDefaultRoute */ true);
+    VerifyExternalRouteInNetData(kDefaultRoute);
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Send RA from router A removing the default route.
+
+    SendRouterAdvert(routerAddressA, DefaultRoute(0, NetworkData::kRoutePreferenceLow));
+
+    AdvanceTime(10000);
+
+    VerifyPrefixTableIsEmpty();
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Check Network Data. Now that router A no longer advertised
+    // a default-route, we should go back to publishing ULA route.
+
+    VerifyOmrPrefixInNetData(omrPrefix, /* aDefaultRoute */ true);
+    VerifyExternalRouteInNetData(kUlaRoute);
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Send RA from router A again advertising a default route.
+
+    SendRouterAdvert(routerAddressA, DefaultRoute(kValidLitime, NetworkData::kRoutePreferenceLow));
+
+    AdvanceTime(10000);
+
+    VerifyPrefixTable({RoutePrefix(defaultRoute, kValidLitime, NetworkData::kRoutePreferenceLow, routerAddressA)});
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Check Network Data. We should see default route published.
+
+    VerifyOmrPrefixInNetData(omrPrefix, /* aDefaultRoute */ true);
+    VerifyExternalRouteInNetData(kDefaultRoute);
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+    Log("End of TestDefaultRoute");
+
+    FinalizeTest();
+}
+
+void TestAdvNonUlaRoute(void)
+{
+    Ip6::Prefix                     localOnLink;
+    Ip6::Prefix                     localOmr;
+    Ip6::Prefix                     omrPrefix      = PrefixFromString("2000:0000:1111:4444::", 64);
+    Ip6::Prefix                     routePrefix    = PrefixFromString("2000:1234:5678::", 64);
+    Ip6::Address                    routerAddressA = AddressFromString("fd00::aaaa");
+    NetworkData::OnMeshPrefixConfig prefixConfig;
+
+    Log("--------------------------------------------------------------------------------------------");
+    Log("TestAdvNonUlaRoute");
+
+    InitTest();
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Start Routing Manager. Check emitted RS and RA messages.
+
+    sRsEmitted   = false;
+    sRaValidated = false;
+    sExpectedPio = kPioAdvertisingLocalOnLink;
+    sExpectedRios.Clear();
+
+    SuccessOrQuit(sInstance->Get<BorderRouter::RoutingManager>().SetEnabled(true));
+
+    SuccessOrQuit(sInstance->Get<BorderRouter::RoutingManager>().GetOnLinkPrefix(localOnLink));
+    SuccessOrQuit(sInstance->Get<BorderRouter::RoutingManager>().GetOmrPrefix(localOmr));
+
+    Log("Local on-link prefix is %s", localOnLink.ToString().AsCString());
+    Log("Local OMR prefix is %s", localOmr.ToString().AsCString());
+
+    sExpectedRios.Add(localOmr);
+
+    AdvanceTime(30000);
+
+    VerifyOrQuit(sRsEmitted);
+    VerifyOrQuit(sRaValidated);
+    VerifyOrQuit(sExpectedRios.SawAll());
+    Log("Received RA was validated");
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Check Network Data to include the local OMR and ULA prefix.
+
+    VerifyOmrPrefixInNetData(localOmr, /* aDefaultRoute */ false);
+    VerifyExternalRouteInNetData(kUlaRoute);
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Send RA from router A advertising a non-ULA.
+
+    SendRouterAdvert(routerAddressA, {Rio(routePrefix, kValidLitime, NetworkData::kRoutePreferenceMedium)});
+
+    AdvanceTime(10000);
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Check the discovered prefix table and ensure the non-ULA
+    // from router A is in the table.
+
+    VerifyPrefixTable({RoutePrefix(routePrefix, kValidLitime, NetworkData::kRoutePreferenceMedium, routerAddressA)});
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Check Network Data. We should not see default route in
+    // Network Data yet since there is no infrastructure-derived
+    // OMR prefix (with preference medium or higher).
+
+    VerifyOmrPrefixInNetData(localOmr, /* aDefaultRoute */ false);
+    VerifyExternalRouteInNetData(kUlaRoute);
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Add an OMR prefix directly into Network Data with
+    // preference medium (infrastructure-derived).
 
     prefixConfig.Clear();
     prefixConfig.mPrefix       = omrPrefix;
@@ -1283,38 +1391,78 @@
     AdvanceTime(10000);
 
     //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-    // Check Network Data. We should again see the default route from
-    // router B included with high preference.
+    // Check Network Data. Now that we have an infrastructure-derived
+    // OMR prefix, the default route should be published.
 
-    VerifyExternalRoutesInNetData({ExternalRoute(onLinkPrefix, NetworkData::kRoutePreferenceMedium),
-                                   ExternalRoute(routePrefix, NetworkData::kRoutePreferenceMedium),
-                                   ExternalRoute(defaultRoute, NetworkData::kRoutePreferenceHigh)});
+    VerifyOmrPrefixInNetData(omrPrefix, /* aDefaultRoute */ true);
+    VerifyExternalRouteInNetData(kDefaultRoute);
 
     //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-    // Send an RA from router B removing default route
-    // in both header and in RIO.
+    // Remove the OMR prefix from Network Data.
 
-    SendRouterAdvert(routerAddressB, {Rio(defaultRoute, 0, NetworkData::kRoutePreferenceHigh)},
-                     DefaultRoute(0, NetworkData::kRoutePreferenceMedium));
+    SuccessOrQuit(otBorderRouterRemoveOnMeshPrefix(sInstance, &omrPrefix));
+    SuccessOrQuit(otBorderRouterRegister(sInstance));
+
     AdvanceTime(10000);
 
     //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-    // Check the discovered prefix table and ensure default route
-    // entry from router B is no longer present.
+    // Check Network Data. We should again go back to ULA prefix. The
+    // non-ULA route advertised by router A should be still present in
+    // the discovered prefix table.
 
-    VerifyPrefixTable({OnLinkPrefix(onLinkPrefix, kValidLitime, kPreferredLifetime, routerAddressA)},
-                      {RoutePrefix(routePrefix, kValidLitime, NetworkData::kRoutePreferenceMedium, routerAddressA)});
+    VerifyOmrPrefixInNetData(localOmr, /* aDefaultRoute */ false);
+    VerifyExternalRouteInNetData(kUlaRoute);
+
+    VerifyPrefixTable({RoutePrefix(routePrefix, kValidLitime, NetworkData::kRoutePreferenceMedium, routerAddressA)});
 
     //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-    // Check Network Data. The default route ::/0 should be now
-    // removed since router B stopped advertising it.
+    // Add the OMR prefix again.
 
-    VerifyExternalRoutesInNetData({ExternalRoute(onLinkPrefix, NetworkData::kRoutePreferenceMedium),
-                                   ExternalRoute(routePrefix, NetworkData::kRoutePreferenceMedium)});
+    SuccessOrQuit(otBorderRouterAddOnMeshPrefix(sInstance, &prefixConfig));
+    SuccessOrQuit(otBorderRouterRegister(sInstance));
+
+    AdvanceTime(10000);
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Check Network Data. Again the default route should be published.
+
+    VerifyOmrPrefixInNetData(omrPrefix, /* aDefaultRoute */ true);
+    VerifyExternalRouteInNetData(kDefaultRoute);
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Send RA from router A removing the route.
+
+    SendRouterAdvert(routerAddressA, {Rio(routePrefix, 0, NetworkData::kRoutePreferenceMedium)});
+
+    AdvanceTime(10000);
+
+    VerifyPrefixTableIsEmpty();
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Check Network Data. Now that router A no longer advertised
+    // the route, we should go back to publishing the ULA route.
+
+    VerifyOmrPrefixInNetData(omrPrefix, /* aDefaultRoute */ true);
+    VerifyExternalRouteInNetData(kUlaRoute);
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Send RA from router A again advertising the route again.
+
+    SendRouterAdvert(routerAddressA, {Rio(routePrefix, kValidLitime, NetworkData::kRoutePreferenceMedium)});
+
+    AdvanceTime(10000);
+
+    VerifyPrefixTable({RoutePrefix(routePrefix, kValidLitime, NetworkData::kRoutePreferenceMedium, routerAddressA)});
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Check Network Data. We should see default route published.
+
+    VerifyOmrPrefixInNetData(omrPrefix, /* aDefaultRoute */ true);
+    VerifyExternalRouteInNetData(kDefaultRoute);
 
     //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 
-    Log("End of TestDefaultRoute");
+    Log("End of TestAdvNonUlaRoute");
 
     FinalizeTest();
 }
@@ -1325,7 +1473,7 @@
 
     Ip6::Prefix  localOnLink;
     Ip6::Prefix  localOmr;
-    Ip6::Prefix  onLinkPrefix   = PrefixFromString("2000:abba:baba::", 64);
+    Ip6::Prefix  onLinkPrefix   = PrefixFromString("fd00:abba:baba::", 64);
     Ip6::Address routerAddressA = AddressFromString("fd00::aaaa");
     uint32_t     localOnLinkLifetime;
 
@@ -1362,8 +1510,8 @@
     //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
     // Check Network Data to include the local OMR and on-link prefix.
 
-    VerifyOmrPrefixInNetData(localOmr);
-    VerifyExternalRoutesInNetData({ExternalRoute(localOnLink, NetworkData::kRoutePreferenceMedium)});
+    VerifyOmrPrefixInNetData(localOmr, /* aDefaultRoute */ false);
+    VerifyExternalRouteInNetData(kUlaRoute);
 
     //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
     // Send an RA from router A with a new on-link (PIO) which is preferred over
@@ -1390,9 +1538,8 @@
     // Check Network Data. We must see the new on-link prefix from router A
     // along with the deprecating local on-link prefix.
 
-    VerifyOmrPrefixInNetData(localOmr);
-    VerifyExternalRoutesInNetData({ExternalRoute(localOnLink, NetworkData::kRoutePreferenceMedium),
-                                   ExternalRoute(onLinkPrefix, NetworkData::kRoutePreferenceMedium)});
+    VerifyOmrPrefixInNetData(localOmr, /* aDefaultRoute */ false);
+    VerifyExternalRouteInNetData(kUlaRoute);
 
     //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
     // Wait for local on-link prefix to expire
@@ -1406,9 +1553,8 @@
         // Ensure Network Data entries remain as before. Mainly we still
         // see the deprecating local on-link prefix.
 
-        VerifyOmrPrefixInNetData(localOmr);
-        VerifyExternalRoutesInNetData({ExternalRoute(localOnLink, NetworkData::kRoutePreferenceMedium),
-                                       ExternalRoute(onLinkPrefix, NetworkData::kRoutePreferenceMedium)});
+        VerifyOmrPrefixInNetData(localOmr, /* aDefaultRoute */ false);
+        VerifyExternalRouteInNetData(kUlaRoute);
 
         // Keep checking the emitted RAs and make sure on-link prefix
         // is included with smaller lifetime every time.
@@ -1443,11 +1589,10 @@
     Log("On-link prefix is now expired");
 
     //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-    // Check Network Data and make sure the the expired local on-link prefix
-    // is removed.
+    // Check Network Data.
 
-    VerifyOmrPrefixInNetData(localOmr);
-    VerifyExternalRoutesInNetData({ExternalRoute(onLinkPrefix, NetworkData::kRoutePreferenceMedium)});
+    VerifyOmrPrefixInNetData(localOmr, /* aDefaultRoute */ false);
+    VerifyExternalRouteInNetData(kUlaRoute);
 
     //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 
@@ -1497,8 +1642,8 @@
     //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
     // Check Network Data to include the local OMR and on-link prefix.
 
-    VerifyOmrPrefixInNetData(localOmr);
-    VerifyExternalRoutesInNetData({ExternalRoute(localOnLink, NetworkData::kRoutePreferenceMedium)});
+    VerifyOmrPrefixInNetData(localOmr, /* aDefaultRoute */ false);
+    VerifyExternalRouteInNetData(kUlaRoute);
 
     //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
     // Add a domain prefix directly into net data. The new prefix should
@@ -1566,8 +1711,8 @@
     // Check Network Data. We should now see that the local OMR prefix
     // is removed.
 
-    VerifyOmrPrefixInNetData(domainPrefix);
-    VerifyExternalRoutesInNetData({ExternalRoute(localOnLink, NetworkData::kRoutePreferenceMedium)});
+    VerifyOmrPrefixInNetData(domainPrefix, /* aDefaultRoute */ false);
+    VerifyExternalRouteInNetData(kUlaRoute);
 
     //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
     // Remove the domain prefix from net data.
@@ -1593,8 +1738,8 @@
     // Check Network Data. We should see that the local OMR prefix is
     // added again.
 
-    VerifyOmrPrefixInNetData(localOmr);
-    VerifyExternalRoutesInNetData({ExternalRoute(localOnLink, NetworkData::kRoutePreferenceMedium)});
+    VerifyOmrPrefixInNetData(localOmr, /* aDefaultRoute */ false);
+    VerifyExternalRouteInNetData(kUlaRoute);
 
     //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 
@@ -1690,12 +1835,10 @@
     oldPrefixLifetime = sDeprecatingPrefixes[0].mLifetime;
 
     //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-    // Validate the Network Data to contain both the current and old
-    // local on-link prefixes.
+    // Validate Network Data.
 
-    VerifyOmrPrefixInNetData(localOmr);
-    VerifyExternalRoutesInNetData({ExternalRoute(localOnLink, NetworkData::kRoutePreferenceMedium),
-                                   ExternalRoute(oldLocalOnLink, NetworkData::kRoutePreferenceMedium)});
+    VerifyOmrPrefixInNetData(localOmr, /* aDefaultRoute */ false);
+    VerifyExternalRouteInNetData(kUlaRoute);
 
     //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
     // Stop BR and validate that a final RA is emitted deprecating
@@ -1717,7 +1860,7 @@
     VerifyOrQuit(!sRaValidated);
 
     VerifyNoOmrPrefixInNetData();
-    VerifyNoExternalRouteInNetData();
+    VerifyExternalRouteInNetData(kNoRoute);
 
     //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
     // Start BR again and validate old prefix will continue to
@@ -1740,11 +1883,9 @@
 
     while (oldPrefixLifetime > 2 * kMaxRaTxInterval)
     {
-        // Ensure Network Data entries remain as before. Mainly we still
-        // see the deprecating local on-link prefix.
+        // Ensure Network Data entries remain as before.
 
-        VerifyExternalRoutesInNetData({ExternalRoute(localOnLink, NetworkData::kRoutePreferenceMedium),
-                                       ExternalRoute(oldLocalOnLink, NetworkData::kRoutePreferenceMedium)});
+        VerifyExternalRouteInNetData(kUlaRoute);
 
         // Keep checking the emitted RAs and make sure the prefix
         // is included with smaller lifetime every time.
@@ -1766,18 +1907,17 @@
 
     sRaValidated = false;
 
-    AdvanceTime(2 * kMaxRaTxInterval * 1000);
+    AdvanceTime(3 * kMaxRaTxInterval * 1000);
 
     VerifyOrQuit(sRaValidated);
     VerifyOrQuit(sDeprecatingPrefixes.IsEmpty());
     Log("Old on-link prefix is now expired");
 
     //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-    // Validate the Network Data now only contains the current local
-    // on-link prefix.
+    // Validate the Network Data.
 
-    VerifyOmrPrefixInNetData(localOmr);
-    VerifyExternalRoutesInNetData({ExternalRoute(localOnLink, NetworkData::kRoutePreferenceMedium)});
+    VerifyOmrPrefixInNetData(localOmr, /* aDefaultRoute */ false);
+    VerifyExternalRouteInNetData(kUlaRoute);
 
     //= = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
     // Check behavior when ext PAN ID changes while the local on-link is being
@@ -1827,12 +1967,10 @@
     VerifyOrQuit(sDeprecatingPrefixes[0].mPrefix == oldLocalOnLink);
 
     //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-    // Validate that Network Data contains the old local on-link
-    // prefix along with entry from router A.
+    // Validate that Network Data.
 
-    VerifyOmrPrefixInNetData(localOmr);
-    VerifyExternalRoutesInNetData({ExternalRoute(oldLocalOnLink, NetworkData::kRoutePreferenceMedium),
-                                   ExternalRoute(onLinkPrefix, NetworkData::kRoutePreferenceMedium)});
+    VerifyOmrPrefixInNetData(localOmr, /* aDefaultRoute */ true);
+    VerifyExternalRouteInNetData(kDefaultRoute);
 
     //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
     // Wait for old local on-link prefix to expire.
@@ -1843,11 +1981,9 @@
 
         SendRouterAdvert(routerAddressA, {Pio(onLinkPrefix, kValidLitime, kPreferredLifetime)});
 
-        // Ensure Network Data entries remain as before. Mainly we still
-        // see the deprecating old local on-link prefix.
+        // Ensure Network Data entries remain as before.
 
-        VerifyExternalRoutesInNetData({ExternalRoute(oldLocalOnLink, NetworkData::kRoutePreferenceMedium),
-                                       ExternalRoute(onLinkPrefix, NetworkData::kRoutePreferenceMedium)});
+        VerifyExternalRouteInNetData(kDefaultRoute);
 
         // Keep checking the emitted RAs and make sure the prefix
         // is included with smaller lifetime every time.
@@ -1868,19 +2004,24 @@
     // longer be seen in the emitted RA message.
 
     SendRouterAdvert(routerAddressA, {Pio(onLinkPrefix, kValidLitime, kPreferredLifetime)});
+
     sRaValidated = false;
 
-    AdvanceTime(2 * kMaxRaTxInterval * 1000);
+    AdvanceTime(kMaxRaTxInterval * 1000);
+    SendRouterAdvert(routerAddressA, {Pio(onLinkPrefix, kValidLitime, kPreferredLifetime)});
+    AdvanceTime(kMaxRaTxInterval * 1000);
+    SendRouterAdvert(routerAddressA, {Pio(onLinkPrefix, kValidLitime, kPreferredLifetime)});
+    AdvanceTime(kMaxRaTxInterval * 1000);
 
     VerifyOrQuit(sRaValidated);
     VerifyOrQuit(sDeprecatingPrefixes.IsEmpty());
     Log("Old on-link prefix is now expired");
 
     //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-    // Validate the Network Data to now only contains entry from router A.
+    // Validate the Network Data.
 
-    VerifyOmrPrefixInNetData(localOmr);
-    VerifyExternalRoutesInNetData({ExternalRoute(onLinkPrefix, NetworkData::kRoutePreferenceMedium)});
+    VerifyOmrPrefixInNetData(localOmr, /* aDefaultRoute */ true);
+    VerifyExternalRouteInNetData(kDefaultRoute);
 
     //= = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
     // Check behavior when ext PAN ID changes while the local on-link is not
@@ -1906,10 +2047,10 @@
     VerifyOrQuit(sDeprecatingPrefixes.IsEmpty());
 
     //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-    // Validate the Network Data to now only contains entry from router A.
+    // Validate the Network Data.
 
-    VerifyOmrPrefixInNetData(localOmr);
-    VerifyExternalRoutesInNetData({ExternalRoute(onLinkPrefix, NetworkData::kRoutePreferenceMedium)});
+    VerifyOmrPrefixInNetData(localOmr, /* aDefaultRoute */ true);
+    VerifyExternalRouteInNetData(kDefaultRoute);
 
     //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
     // Remove the on-link prefix PIO being advertised by router A
@@ -1926,11 +2067,11 @@
 
     //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
     // Wait for longer than valid lifetime of PIO entry from router A.
-    // Validate that it is unpublished from network data.
+    // Validate that default route is unpublished from network data.
 
     AdvanceTime(2000 * 1000);
-    VerifyOmrPrefixInNetData(localOmr);
-    VerifyExternalRoutesInNetData({ExternalRoute(localOnLink, NetworkData::kRoutePreferenceMedium)});
+    VerifyOmrPrefixInNetData(localOmr, /* aDefaultRoute */ false);
+    VerifyExternalRouteInNetData(kUlaRoute);
 
     //= = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
     // Multiple PAN ID changes and multiple deprecating old prefixes.
@@ -1949,8 +2090,7 @@
     VerifyOrQuit(sDeprecatingPrefixes.GetLength() == 1);
     VerifyOrQuit(sDeprecatingPrefixes.ContainsMatching(oldPrefixes[0]));
 
-    VerifyExternalRoutesInNetData({ExternalRoute(localOnLink, NetworkData::kRoutePreferenceMedium),
-                                   ExternalRoute(oldPrefixes[0], NetworkData::kRoutePreferenceMedium)});
+    VerifyExternalRouteInNetData(kUlaRoute);
 
     //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
     // Change the prefix again. We should see two deprecating prefixes.
@@ -1970,9 +2110,7 @@
     VerifyOrQuit(sDeprecatingPrefixes.ContainsMatching(oldPrefixes[0]));
     VerifyOrQuit(sDeprecatingPrefixes.ContainsMatching(oldPrefixes[1]));
 
-    VerifyExternalRoutesInNetData({ExternalRoute(localOnLink, NetworkData::kRoutePreferenceMedium),
-                                   ExternalRoute(oldPrefixes[0], NetworkData::kRoutePreferenceMedium),
-                                   ExternalRoute(oldPrefixes[1], NetworkData::kRoutePreferenceMedium)});
+    VerifyExternalRouteInNetData(kUlaRoute);
 
     //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
     // Wait for 15 minutes and then change ext PAN ID again.
@@ -1996,10 +2134,7 @@
     VerifyOrQuit(sDeprecatingPrefixes.ContainsMatching(oldPrefixes[1]));
     VerifyOrQuit(sDeprecatingPrefixes.ContainsMatching(oldPrefixes[2]));
 
-    VerifyExternalRoutesInNetData({ExternalRoute(localOnLink, NetworkData::kRoutePreferenceMedium),
-                                   ExternalRoute(oldPrefixes[0], NetworkData::kRoutePreferenceMedium),
-                                   ExternalRoute(oldPrefixes[1], NetworkData::kRoutePreferenceMedium),
-                                   ExternalRoute(oldPrefixes[2], NetworkData::kRoutePreferenceMedium)});
+    VerifyExternalRouteInNetData(kUlaRoute);
 
     //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
     // Change ext PAN ID back to previous value of `kExtPanId1`.
@@ -2023,10 +2158,7 @@
     VerifyOrQuit(oldPrefixes[2] == localOnLink);
     VerifyOrQuit(sDeprecatingPrefixes.ContainsMatching(oldPrefixes[3]));
 
-    VerifyExternalRoutesInNetData({ExternalRoute(localOnLink, NetworkData::kRoutePreferenceMedium),
-                                   ExternalRoute(oldPrefixes[0], NetworkData::kRoutePreferenceMedium),
-                                   ExternalRoute(oldPrefixes[1], NetworkData::kRoutePreferenceMedium),
-                                   ExternalRoute(oldPrefixes[3], NetworkData::kRoutePreferenceMedium)});
+    VerifyExternalRouteInNetData(kUlaRoute);
 
     //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
     // Stop BR and validate the final emitted RA to contain
@@ -2045,7 +2177,7 @@
     VerifyOrQuit(sDeprecatingPrefixes.ContainsMatching(oldPrefixes[3]));
 
     VerifyNoOmrPrefixInNetData();
-    VerifyNoExternalRouteInNetData();
+    VerifyExternalRouteInNetData(kNoRoute);
 
     //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
     // Wait for 15 minutes while BR stays disabled and validate
@@ -2058,7 +2190,7 @@
     VerifyOrQuit(!sRaValidated);
 
     VerifyNoOmrPrefixInNetData();
-    VerifyNoExternalRouteInNetData();
+    VerifyExternalRouteInNetData(kNoRoute);
 
     //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
     // Start BR again, and check that we only see the last deprecating prefix
@@ -2076,8 +2208,7 @@
     VerifyOrQuit(sDeprecatingPrefixes.GetLength() == 1);
     VerifyOrQuit(sDeprecatingPrefixes.ContainsMatching(oldPrefixes[3]));
 
-    VerifyExternalRoutesInNetData({ExternalRoute(localOnLink, NetworkData::kRoutePreferenceMedium),
-                                   ExternalRoute(oldPrefixes[3], NetworkData::kRoutePreferenceMedium)});
+    VerifyExternalRouteInNetData(kUlaRoute);
 
     //= = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
     // Validate the oldest prefix is removed when we have too many
@@ -2112,10 +2243,7 @@
     VerifyOrQuit(sDeprecatingPrefixes.ContainsMatching(oldPrefixes[2]));
     VerifyOrQuit(!sDeprecatingPrefixes.ContainsMatching(oldLocalOnLink));
 
-    VerifyExternalRoutesInNetData({ExternalRoute(localOnLink, NetworkData::kRoutePreferenceMedium),
-                                   ExternalRoute(oldPrefixes[0], NetworkData::kRoutePreferenceMedium),
-                                   ExternalRoute(oldPrefixes[1], NetworkData::kRoutePreferenceMedium),
-                                   ExternalRoute(oldPrefixes[2], NetworkData::kRoutePreferenceMedium)});
+    VerifyExternalRouteInNetData(kUlaRoute);
 
     //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 
@@ -2316,8 +2444,8 @@
     //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
     // Check Network Data to include the local OMR and on-link prefix.
 
-    VerifyOmrPrefixInNetData(localOmr);
-    VerifyExternalRoutesInNetData({ExternalRoute(localOnLink, NetworkData::kRoutePreferenceMedium)});
+    VerifyOmrPrefixInNetData(localOmr, /* aDefaultRoute */ false);
+    VerifyExternalRouteInNetData(kUlaRoute);
 
     //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
     // Send an RA from router A with our local on-link prefix as RIO.
@@ -2333,10 +2461,10 @@
     VerifyOrQuit(sRaValidated);
 
     //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-    // Check Network Data to still include the local OMR and on-link prefix.
+    // Check Network Data to still include the local OMR and ULA prefix.
 
-    VerifyOmrPrefixInNetData(localOmr);
-    VerifyExternalRoutesInNetData({ExternalRoute(localOnLink, NetworkData::kRoutePreferenceMedium)});
+    VerifyOmrPrefixInNetData(localOmr, /* aDefaultRoute */ false);
+    VerifyExternalRouteInNetData(kUlaRoute);
 
     //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
     // Send an RA from router A removing local on-link prefix as RIO.
@@ -2344,11 +2472,11 @@
     SendRouterAdvert(routerAddressA, {Rio(localOnLink, 0, NetworkData::kRoutePreferenceMedium)});
 
     //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-    // Verify that on-link prefix is still included in Network Data and
+    // Verify that ULA prefix is still included in Network Data and
     // the change by router A did not cause it to be unpublished.
 
     AdvanceTime(10000);
-    VerifyExternalRoutesInNetData({ExternalRoute(localOnLink, NetworkData::kRoutePreferenceMedium)});
+    VerifyExternalRouteInNetData(kUlaRoute);
 
     //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
     // Check that the local on-link prefix is still being advertised.
@@ -2357,7 +2485,7 @@
     AdvanceTime(610000);
     VerifyOrQuit(sRaValidated);
 
-    VerifyExternalRoutesInNetData({ExternalRoute(localOnLink, NetworkData::kRoutePreferenceMedium)});
+    VerifyExternalRouteInNetData(kUlaRoute);
 
     //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
     // Send RA from router B advertising an on-link prefix. This
@@ -2377,12 +2505,11 @@
     Log("On-link prefix is deprecating, remaining lifetime:%d", sOnLinkLifetime);
 
     //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-    // Check Network Data to include the new on-link prefix from router B and
-    // the deprecating local on-link prefix.
+    // Check Network Data to include the default route now due
+    // the new on-link prefix from router B.
 
-    VerifyOmrPrefixInNetData(localOmr);
-    VerifyExternalRoutesInNetData({ExternalRoute(localOnLink, NetworkData::kRoutePreferenceMedium),
-                                   ExternalRoute(onLinkPrefix, NetworkData::kRoutePreferenceMedium)});
+    VerifyOmrPrefixInNetData(localOmr, /* aDefaultRoute */ true);
+    VerifyExternalRouteInNetData(kDefaultRoute);
 
     //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
     // Send an RA from router A again adding local on-link prefix as RIO.
@@ -2400,8 +2527,7 @@
     //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
     // Check Network Data remains unchanged.
 
-    VerifyExternalRoutesInNetData({ExternalRoute(localOnLink, NetworkData::kRoutePreferenceMedium),
-                                   ExternalRoute(onLinkPrefix, NetworkData::kRoutePreferenceMedium)});
+    VerifyExternalRouteInNetData(kDefaultRoute);
 
     //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
     // Send an RA from router A removing the previous RIO.
@@ -2409,12 +2535,10 @@
     SendRouterAdvert(routerAddressA, {Rio(localOnLink, 0, NetworkData::kRoutePreferenceMedium)});
 
     //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-    // Check Network Data remains unchanged and still contains
-    // the deprecating local on-link prefix.
+    // Check Network Data remains unchanged.
 
     AdvanceTime(60000);
-    VerifyExternalRoutesInNetData({ExternalRoute(localOnLink, NetworkData::kRoutePreferenceMedium),
-                                   ExternalRoute(onLinkPrefix, NetworkData::kRoutePreferenceMedium)});
+    VerifyExternalRouteInNetData(kDefaultRoute);
 
     //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
     // Send RA from router B removing its on-link prefix.
@@ -2431,10 +2555,9 @@
     VerifyOrQuit(sRaValidated);
 
     //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-    // Check Network Data still contains both prefixes.
+    // Check Network Data to remain unchanged.
 
-    VerifyExternalRoutesInNetData({ExternalRoute(localOnLink, NetworkData::kRoutePreferenceMedium),
-                                   ExternalRoute(onLinkPrefix, NetworkData::kRoutePreferenceMedium)});
+    VerifyExternalRouteInNetData(kDefaultRoute);
 
     //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
     // Change the extended PAN ID.
@@ -2456,12 +2579,10 @@
         oldLocalOnLink.ToString().AsCString());
 
     //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-    // Check Network Data contains old and new local on-link prefix
-    // and deprecating prefix from router B.
+    // Check Network Data contains default route due to the
+    // deprecating on-link prefix from router B.
 
-    VerifyExternalRoutesInNetData({ExternalRoute(localOnLink, NetworkData::kRoutePreferenceMedium),
-                                   ExternalRoute(oldLocalOnLink, NetworkData::kRoutePreferenceMedium),
-                                   ExternalRoute(onLinkPrefix, NetworkData::kRoutePreferenceMedium)});
+    VerifyExternalRouteInNetData(kDefaultRoute);
 
     //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
     // Send an RA from router A again adding the old local on-link prefix
@@ -2473,9 +2594,7 @@
     // Check Network Data remains unchanged.
 
     AdvanceTime(10000);
-    VerifyExternalRoutesInNetData({ExternalRoute(localOnLink, NetworkData::kRoutePreferenceMedium),
-                                   ExternalRoute(oldLocalOnLink, NetworkData::kRoutePreferenceMedium),
-                                   ExternalRoute(onLinkPrefix, NetworkData::kRoutePreferenceMedium)});
+    VerifyExternalRouteInNetData(kDefaultRoute);
 
     //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
     // Send an RA from router A removing the previous RIO.
@@ -2486,9 +2605,7 @@
     // Check Network Data remains unchanged.
 
     AdvanceTime(10000);
-    VerifyExternalRoutesInNetData({ExternalRoute(localOnLink, NetworkData::kRoutePreferenceMedium),
-                                   ExternalRoute(oldLocalOnLink, NetworkData::kRoutePreferenceMedium),
-                                   ExternalRoute(onLinkPrefix, NetworkData::kRoutePreferenceMedium)});
+    VerifyExternalRouteInNetData(kDefaultRoute);
 
     //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 
@@ -2537,10 +2654,10 @@
     VerifyOrQuit(sExpectedRios.SawAll());
 
     //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-    // Check Network Data to include the local OMR and on-link prefix.
+    // Check Network Data to include the local OMR and ULA prefix.
 
-    VerifyOmrPrefixInNetData(localOmr);
-    VerifyExternalRoutesInNetData({ExternalRoute(localOnLink, NetworkData::kRoutePreferenceMedium)});
+    VerifyOmrPrefixInNetData(localOmr, /* aDefaultRoute */ false);
+    VerifyExternalRouteInNetData(kUlaRoute);
 
     //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
     // Disable the instance and re-enable it.
@@ -2562,10 +2679,10 @@
     VerifyOrQuit(sExpectedRios.SawAll());
 
     //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-    // Check Network Data to include the local OMR and on-link prefix.
+    // Check Network Data to include the local OMR and ULA prefix.
 
-    VerifyOmrPrefixInNetData(localOmr);
-    VerifyExternalRoutesInNetData({ExternalRoute(localOnLink, NetworkData::kRoutePreferenceMedium)});
+    VerifyOmrPrefixInNetData(localOmr, /* aDefaultRoute */ false);
+    VerifyExternalRouteInNetData(kUlaRoute);
 
     //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
     // Send RA from router A advertising an on-link prefix.
@@ -2598,10 +2715,10 @@
     VerifyOrQuit(sExpectedRios.SawAll());
 
     //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-    // Check Network Data to include the local OMR and on-link prefix.
+    // Check Network Data to include the local OMR and ULA prefix.
 
-    VerifyOmrPrefixInNetData(localOmr);
-    VerifyExternalRoutesInNetData({ExternalRoute(localOnLink, NetworkData::kRoutePreferenceMedium)});
+    VerifyOmrPrefixInNetData(localOmr, /* aDefaultRoute */ false);
+    VerifyExternalRouteInNetData(kUlaRoute);
 
     //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 
@@ -2659,12 +2776,10 @@
     VerifyOrQuit(sDeprecatingPrefixes.GetLength() == 1);
 
     //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-    // Check the saved prefixes are in netdata and being
-    // deprecated.
+    // Check Network Data to now use default route due to the
+    // on-link prefix from router A.
 
-    VerifyExternalRoutesInNetData({ExternalRoute(localOnLink, NetworkData::kRoutePreferenceMedium),
-                                   ExternalRoute(oldLocalOnLink, NetworkData::kRoutePreferenceMedium),
-                                   ExternalRoute(onLinkPrefix, NetworkData::kRoutePreferenceMedium)});
+    VerifyExternalRouteInNetData(kDefaultRoute);
 
     //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
     // Wait for more than 1800 seconds to let the deprecating
@@ -2676,7 +2791,7 @@
         AdvanceTime(10 * 1000);
     }
 
-    VerifyExternalRoutesInNetData({ExternalRoute(onLinkPrefix, NetworkData::kRoutePreferenceMedium)});
+    VerifyExternalRouteInNetData(kDefaultRoute);
 
     //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
     // Disable the instance and re-enable it and restart Routing Manager.
@@ -2703,11 +2818,9 @@
     VerifyOrQuit(sDeprecatingPrefixes.GetLength() == 0);
 
     //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-    // Check that previously saved on-link prefixes are no longer
-    // seen in Network Data (indicating that they were removed from
-    // `Settings`).
+    // Check Network Data still contains the default route.
 
-    VerifyExternalRoutesInNetData({ExternalRoute(onLinkPrefix, NetworkData::kRoutePreferenceMedium)});
+    VerifyExternalRouteInNetData(kDefaultRoute);
 
     //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 
@@ -2923,6 +3036,88 @@
 }
 #endif // OPENTHREAD_CONFIG_SRP_SERVER_ENABLE
 
+#if OPENTHREAD_CONFIG_NAT64_BORDER_ROUTING_ENABLE
+void TestNat64PrefixSelection(void)
+{
+    Ip6::Prefix                     localNat64;
+    Ip6::Prefix                     ailNat64 = PrefixFromString("2000:0:0:1:0:0::", 96);
+    Ip6::Prefix                     localOmr;
+    Ip6::Prefix                     omrPrefix = PrefixFromString("2000:0000:1111:4444::", 64);
+    NetworkData::OnMeshPrefixConfig prefixConfig;
+
+    Log("--------------------------------------------------------------------------------------------");
+    Log("TestNat64PrefixSelection");
+
+    InitTest();
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Start Routing Manager. Check local NAT64 prefix generation.
+
+    SuccessOrQuit(sInstance->Get<BorderRouter::RoutingManager>().SetEnabled(true));
+    SuccessOrQuit(sInstance->Get<BorderRouter::RoutingManager>().GetNat64Prefix(localNat64));
+    SuccessOrQuit(sInstance->Get<BorderRouter::RoutingManager>().GetOmrPrefix(localOmr));
+
+    Log("Local nat64 prefix is %s", localNat64.ToString().AsCString());
+    Log("Local OMR prefix is %s", localOmr.ToString().AsCString());
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Enable Nat64 Prefix Manager. Check local NAT64 prefix in Network Data.
+
+    sInstance->Get<BorderRouter::RoutingManager>().SetNat64PrefixManagerEnabled(true);
+
+    AdvanceTime(20000);
+
+    VerifyOmrPrefixInNetData(localOmr, /* aDefaultRoute */ false);
+    VerifyNat64PrefixInNetData(localNat64);
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // AIL NAT64 prefix discovered. No infra-derived OMR prefix in Network Data.
+    // Check local NAT64 prefix in Network Data.
+
+    DiscoverNat64Prefix(ailNat64);
+
+    AdvanceTime(20000);
+
+    VerifyNat64PrefixInNetData(localNat64);
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // Add a medium preference OMR prefix into Network Data.
+    // Check AIL NAT64 prefix published in Network Data.
+
+    prefixConfig.Clear();
+    prefixConfig.mPrefix       = omrPrefix;
+    prefixConfig.mStable       = true;
+    prefixConfig.mSlaac        = true;
+    prefixConfig.mPreferred    = true;
+    prefixConfig.mOnMesh       = true;
+    prefixConfig.mDefaultRoute = false;
+    prefixConfig.mPreference   = NetworkData::kRoutePreferenceMedium;
+
+    SuccessOrQuit(otBorderRouterAddOnMeshPrefix(sInstance, &prefixConfig));
+    SuccessOrQuit(otBorderRouterRegister(sInstance));
+
+    AdvanceTime(20000);
+
+    VerifyOmrPrefixInNetData(omrPrefix, /* aDefaultRoute */ false);
+    VerifyNat64PrefixInNetData(ailNat64);
+
+    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    // AIL NAT64 prefix removed.
+    // Check local NAT64 prefix in Network Data.
+
+    ailNat64.Clear();
+    DiscoverNat64Prefix(ailNat64);
+
+    AdvanceTime(20000);
+
+    VerifyOmrPrefixInNetData(omrPrefix, /* aDefaultRoute */ false);
+    VerifyNat64PrefixInNetData(localNat64);
+
+    Log("End of TestNat64PrefixSelection");
+    FinalizeTest();
+}
+#endif // OPENTHREAD_CONFIG_NAT64_BORDER_ROUTING_ENABLE
+
 #endif // OPENTHREAD_CONFIG_BORDER_ROUTING_ENABLE
 
 int main(void)
@@ -2931,6 +3126,7 @@
     TestSamePrefixesFromMultipleRouters();
     TestOmrSelection();
     TestDefaultRoute();
+    TestAdvNonUlaRoute();
     TestLocalOnLinkPrefixDeprecation();
 #if OPENTHREAD_CONFIG_BACKBONE_ROUTER_ENABLE
     TestDomainPrefixAsOmr();
@@ -2944,6 +3140,9 @@
 #if OPENTHREAD_CONFIG_SRP_SERVER_ENABLE
     TestAutoEnableOfSrpServer();
 #endif
+#if OPENTHREAD_CONFIG_NAT64_BORDER_ROUTING_ENABLE
+    TestNat64PrefixSelection();
+#endif
 
     printf("All tests passed\n");
 #else
diff --git a/tests/unit/test_spinel_encoder.cpp b/tests/unit/test_spinel_encoder.cpp
index 7acb0d0..4b76d4c 100644
--- a/tests/unit/test_spinel_encoder.cpp
+++ b/tests/unit/test_spinel_encoder.cpp
@@ -310,7 +310,7 @@
     printf(" -- PASS\n");
 
     printf("\n- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
-    printf("\nTest 5: Test saving position and reseting back to a saved position");
+    printf("\nTest 5: Test saving position and resetting back to a saved position");
 
     SuccessOrQuit(encoder.BeginFrame(Spinel::Buffer::kPriorityLow));
     SuccessOrQuit(encoder.WriteUint8(kUint8));
diff --git a/tests/unit/test_string.cpp b/tests/unit/test_string.cpp
index bd7fb8d..80ff3b0 100644
--- a/tests/unit/test_string.cpp
+++ b/tests/unit/test_string.cpp
@@ -309,6 +309,63 @@
     printf(" -- PASS\n");
 }
 
+void TestStringParseUint8(void)
+{
+    struct TestCase
+    {
+        const char *mString;
+        Error       mError;
+        uint8_t     mExpectedValue;
+        uint16_t    mParsedLength;
+    };
+
+    static const TestCase kTestCases[] = {
+        {"0", kErrorNone, 0, 1},
+        {"1", kErrorNone, 1, 1},
+        {"12", kErrorNone, 12, 2},
+        {"91", kErrorNone, 91, 2},
+        {"200", kErrorNone, 200, 3},
+        {"00000", kErrorNone, 0, 5},
+        {"00000255", kErrorNone, 255, 8},
+        {"2 00", kErrorNone, 2, 1},
+        {"77a12", kErrorNone, 77, 2},
+        {"", kErrorParse},     // Does not start with digit char ['0'-'9']
+        {"a12", kErrorParse},  // Does not start with digit char ['0'-'9']
+        {" 12", kErrorParse},  // Does not start with digit char ['0'-'9']
+        {"256", kErrorParse},  // Larger than max `uint8_t`
+        {"1000", kErrorParse}, // Larger than max `uint8_t`
+        {"0256", kErrorParse}, // Larger than max `uint8_t`
+    };
+
+    printf("\nTest 11: TestStringParseUint8() function\n");
+
+    for (const TestCase &testCase : kTestCases)
+    {
+        const char *string = testCase.mString;
+        Error       error;
+        uint8_t     u8;
+
+        error = StringParseUint8(string, u8);
+
+        VerifyOrQuit(error == testCase.mError);
+
+        if (testCase.mError == kErrorNone)
+        {
+            printf("\n%-10s -> %-3u (expect: %-3u), len:%u (expect:%u)", testCase.mString, u8, testCase.mExpectedValue,
+                   static_cast<uint8_t>(string - testCase.mString), testCase.mParsedLength);
+
+            VerifyOrQuit(u8 == testCase.mExpectedValue);
+            VerifyOrQuit(string - testCase.mString == testCase.mParsedLength);
+        }
+        else
+        {
+            printf("\n%-10s -> kErrorParse", testCase.mString);
+        }
+    }
+
+    printf("\n\n -- PASS\n");
+}
+
 // gcc-4 does not support constexpr function
 #if __GNUC__ > 4
 static_assert(ot::AreStringsInOrder("a", "b"), "AreStringsInOrder() failed");
@@ -331,6 +388,7 @@
     ot::TestStringEndsWith();
     ot::TestStringMatch();
     ot::TestStringToLowercase();
+    ot::TestStringParseUint8();
     printf("\nAll tests passed.\n");
     return 0;
 }
diff --git a/tests/unit/test_timer.cpp b/tests/unit/test_timer.cpp
index b6ecd66..26b55d8 100644
--- a/tests/unit/test_timer.cpp
+++ b/tests/unit/test_timer.cpp
@@ -507,7 +507,7 @@
     }
 
     // given the order in which timers are started, the TimerScheduler should call otPlatAlarmMilliStartAt 2 times.
-    // one for timer[0] and one for timer[5] which will supercede timer[0].
+    // one for timer[0] and one for timer[5] which will supersede timer[0].
     VerifyOrQuit(sCallCount[kCallCountIndexAlarmStart] == 2, "TestTenTimer: Start CallCount Failed.");
     VerifyOrQuit(sCallCount[kCallCountIndexAlarmStop] == 0, "TestTenTimer: Stop CallCount Failed.");
     VerifyOrQuit(sCallCount[kCallCountIndexTimerHandler] == 0, "TestTenTimer: Handler CallCount Failed.");
diff --git a/third_party/CMakeLists.txt b/third_party/CMakeLists.txt
index 7191432..d57660f 100644
--- a/third_party/CMakeLists.txt
+++ b/third_party/CMakeLists.txt
@@ -30,6 +30,4 @@
     add_subdirectory(mbedtls)
 endif()
 
-if(NOT OT_EXCLUDE_TCPLP_LIB)
-    add_subdirectory(tcplp)
-endif()
+add_subdirectory(tcplp)
diff --git a/third_party/Makefile.am b/third_party/Makefile.am
index 5e37431..825f6e8 100644
--- a/third_party/Makefile.am
+++ b/third_party/Makefile.am
@@ -30,7 +30,6 @@
 
 EXTRA_DIST                              = \
     nlbuild-autotools                     \
-    openthread-test-driver                \
     $(NULL)
 
 # Always package (e.g. for 'make dist') these subdirectories.
diff --git a/third_party/mbedtls/CMakeLists.txt b/third_party/mbedtls/CMakeLists.txt
index e58ef81..f32df16 100644
--- a/third_party/mbedtls/CMakeLists.txt
+++ b/third_party/mbedtls/CMakeLists.txt
@@ -45,6 +45,8 @@
 string(REPLACE "-Wconversion" "" CMAKE_C_FLAGS "${CMAKE_C_FLAGS}")
 string(REPLACE "-Wconversion" "" CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}")
 
+set(MBEDTLS_FATAL_WARNINGS OFF CACHE BOOL "Compiler warnings treated as errors" FORCE)
+
 add_subdirectory(repo)
 
 if(UNIFDEFALL_EXE AND SED_EXE AND UNIFDEF_VERSION VERSION_GREATER_EQUAL 2.10)
diff --git a/tools/harness-simulation/harness/Thread_Harness/THCI/OpenThread_Sim.py b/tools/harness-simulation/harness/Thread_Harness/THCI/OpenThread_Sim.py
index 18c622e..c8e84ae 100644
--- a/tools/harness-simulation/harness/Thread_Harness/THCI/OpenThread_Sim.py
+++ b/tools/harness-simulation/harness/Thread_Harness/THCI/OpenThread_Sim.py
@@ -89,7 +89,7 @@
         self.__stdout.channel.setblocking(0)
 
         # Some commands such as `udp send <ip> -x <hex>` send binary data
-        # The UDP packet recevier will output the data in binary to stdout
+        # The UDP packet receiver will output the data in binary to stdout
         self.__stdout._set_mode('rb')
 
     def __disconnect(self, dwCtrlType):
diff --git a/tools/harness-sniffer/OT_Sniffer.py b/tools/harness-sniffer/OT_Sniffer.py
index 78da15a..2c97d6c 100644
--- a/tools/harness-sniffer/OT_Sniffer.py
+++ b/tools/harness-sniffer/OT_Sniffer.py
@@ -43,7 +43,7 @@
             self.is_active = False
 
         except Exception as e:
-            ModuleHelper.WriteIntoDebugLogger('OT_Sniffer: [intialize] --> ' + str(e))
+            ModuleHelper.WriteIntoDebugLogger('OT_Sniffer: [initialize] --> ' + str(e))
 
     def discoverSniffer(self):
         sniffers = []
@@ -148,7 +148,7 @@
 
     def getSnifferAddress(self):
         """
-        Method to retrun the current sniffer's COM/IP address
+        Method to return the current sniffer's COM/IP address
         @return : string
         """
         return self.port
diff --git a/tools/harness-thci/OpenThread.py b/tools/harness-thci/OpenThread.py
index 2410bc4..1e4edbb 100644
--- a/tools/harness-thci/OpenThread.py
+++ b/tools/harness-thci/OpenThread.py
@@ -72,7 +72,7 @@
 OT12_VERSION = 'OPENTHREAD'
 OT13_VERSION = 'OPENTHREAD'
 
-# Supported device capabilites in this THCI implementation
+# Supported device capabilities in this THCI implementation
 OT11_CAPBS = DevCapb.V1_1
 OT12_CAPBS = (DevCapb.L_AIO | DevCapb.C_FFD | DevCapb.C_RFD)
 OT12BR_CAPBS = (DevCapb.C_BBR | DevCapb.C_Host | DevCapb.C_Comm)
@@ -225,7 +225,7 @@
             **kwargs: Arbitrary keyword arguments
                       Includes 'EUI' and 'SerialPort'
         """
-        self.intialize(kwargs)
+        self.initialize(kwargs)
 
     @abstractmethod
     def _connect(self):
@@ -255,7 +255,7 @@
             line str: data send to device
         """
 
-    # Override the following empty methods in the dervied classes when needed
+    # Override the following empty methods in the derived classes when needed
     def _onCommissionStart(self):
         """Called when commissioning starts"""
 
@@ -399,7 +399,7 @@
         time.sleep(duration)
 
     @API
-    def intialize(self, params):
+    def initialize(self, params):
         """initialize the serial port with baudrate, timeout parameters"""
         self.mac = params.get('EUI')
         self.backboneNetif = params.get('Param8') or 'eth0'
@@ -442,7 +442,7 @@
                                 self.UIStatusMsg)
             ModuleHelper.WriteIntoDebugLogger('Err: OpenThread device Firmware not matching..')
 
-        # Make this class compatible with Thread referenece 20200818
+        # Make this class compatible with Thread reference 20200818
         self.__detectReference20200818()
 
     def __repr__(self):
@@ -880,7 +880,7 @@
 
     @API
     def setMAC(self, xEUI):
-        """set the extended addresss of Thread device
+        """set the extended address of Thread device
 
         Args:
             xEUI: extended address in hex format
@@ -2369,7 +2369,7 @@
         Args:
             sAddr: IPv6 destination address for this message
             xCommissionerSessionId: commissioner session id
-            listChannelMask: a channel array to indicate which channels to be scaned
+            listChannelMask: a channel array to indicate which channels to be scanned
             xCount: number of IEEE 802.15.4 ED Scans (milliseconds)
             xPeriod: Period between successive IEEE802.15.4 ED Scans (milliseconds)
             xScanDuration: ScanDuration when performing an IEEE 802.15.4 ED Scan (milliseconds)
diff --git a/tools/harness-thci/OpenThread_BR.py b/tools/harness-thci/OpenThread_BR.py
index 83e16bf..c22557a 100644
--- a/tools/harness-thci/OpenThread_BR.py
+++ b/tools/harness-thci/OpenThread_BR.py
@@ -708,7 +708,7 @@
     @API
     def stopListeningToAddr(self, sAddr):
         """
-        Unsubscribe to a given IPv6 address which was subscribed earlier wiht `registerMulticast`.
+        Unsubscribe to a given IPv6 address which was subscribed earlier with `registerMulticast`.
 
         Args:
             sAddr   : str : Multicast address to be unsubscribed. Use an empty string to unsubscribe
diff --git a/tools/harness-thci/OpenThread_WpanCtl.py b/tools/harness-thci/OpenThread_WpanCtl.py
index 42d8aa9..1d41fe2 100644
--- a/tools/harness-thci/OpenThread_WpanCtl.py
+++ b/tools/harness-thci/OpenThread_WpanCtl.py
@@ -90,7 +90,7 @@
                 self.password = kwargs.get('Param7').strip() if kwargs.get('Param7') else None
             else:
                 self.port = kwargs.get('SerialPort')
-            self.intialize()
+            self.initialize()
         except Exception as e:
             ModuleHelper.WriteIntoDebugLogger('initialize() Error: ' + str(e))
 
@@ -794,7 +794,7 @@
         except Exception as e:
             ModuleHelper.WriteIntoDebugLogger('closeConnection() Error: ' + str(e))
 
-    def intialize(self):
+    def initialize(self):
         """initialize the serial port with baudrate, timeout parameters"""
         print('%s call intialize' % self.port)
         try:
@@ -867,7 +867,7 @@
         return self.__sendCommand(self.wpan_cmd_prefix + 'getprop -v NCP:Channel')[0]
 
     def setMAC(self, xEUI):
-        """set the extended addresss of Thread device
+        """set the extended address of Thread device
 
         Args:
             xEUI: extended address in hex format
@@ -2180,7 +2180,7 @@
         Args:
             sAddr: IPv6 destination address for this message
             xCommissionerSessionId: commissioner session id
-            listChannelMask: a channel array to indicate which channels to be scaned
+            listChannelMask: a channel array to indicate which channels to be scanned
             xCount: number of IEEE 802.15.4 ED Scans (milliseconds)
             xPeriod: Period between successive IEEE802.15.4 ED Scans (milliseconds)
             xScanDuration: IEEE 802.15.4 ScanDuration to use when performing an IEEE 802.15.4 ED Scan (milliseconds)
diff --git a/tools/otci/otci/__init__.py b/tools/otci/otci/__init__.py
index 1952b0a..7cb0aa4 100644
--- a/tools/otci/otci/__init__.py
+++ b/tools/otci/otci/__init__.py
@@ -29,6 +29,7 @@
 
 from . import errors
 from .constants import THREAD_VERSION_1_1, THREAD_VERSION_1_2
+from .command_handlers import OTCommandHandler
 from .otci import OTCI
 from .otci import \
     connect_cli_sim, \
@@ -49,5 +50,13 @@
     'connect_cmd_handler',
 ]
 
-__all__ = ['OTCI', 'errors', 'Rloc16', 'ChildId', 'NetifIdentifer', 'THREAD_VERSION_1_1', 'THREAD_VERSION_1_2'
-          ] + _connectors
+__all__ = [
+    'OTCI',
+    'OTCommandHandler',
+    'errors',
+    'Rloc16',
+    'ChildId',
+    'NetifIdentifier',
+    'THREAD_VERSION_1_1',
+    'THREAD_VERSION_1_2',
+] + _connectors
diff --git a/tools/otci/otci/command_handlers.py b/tools/otci/otci/command_handlers.py
index 3955cd5..7c790e6 100644
--- a/tools/otci/otci/command_handlers.py
+++ b/tools/otci/otci/command_handlers.py
@@ -31,7 +31,7 @@
 import re
 import threading
 import time
-from abc import abstractmethod
+from abc import abstractmethod, ABC
 from typing import Any, Callable, Optional, Union, List, Pattern
 
 from .connectors import OtCliHandler
@@ -39,7 +39,7 @@
 from .utils import match_line
 
 
-class OTCommandHandler:
+class OTCommandHandler(ABC):
     """This abstract class defines interfaces of a OT Command Handler."""
 
     @abstractmethod
@@ -50,12 +50,10 @@
         Note: each line SHOULD NOT contain '\r\n' at the end. The last line of output should be 'Done' or
         'Error <code>: <msg>' following OT CLI conventions.
         """
-        pass
 
     @abstractmethod
     def close(self):
         """Method close should close the OT Command Handler."""
-        pass
 
     @abstractmethod
     def wait(self, duration: float) -> List[str]:
@@ -64,7 +62,6 @@
         Normally, OT CLI does not output when it's not executing any command. But OT CLI can also output
         asynchronously in some cases (e.g. `Join Success` when Joiner joins successfully).
         """
-        pass
 
     @abstractmethod
     def set_line_read_callback(self, callback: Optional[Callable[[str], Any]]):
@@ -85,7 +82,7 @@
         r'(Done|Error|Error \d+:.*|.*: command not found)$')  # "Error" for spinel-cli.py
 
     __PATTERN_LOG_LINE = re.compile(r'((\[(NONE|CRIT|WARN|NOTE|INFO|DEBG)\])'
-                                    r'|(-.*-+: )'  # e.g. -CLI-----: 
+                                    r'|(-.*-+: )'  # e.g. -CLI-----:
                                     r'|(\[[DINWC\-]\] (?=[\w\-]{14}:)\w+-*:)'  # e.g. [I] Mac-----------:
                                     r')')
     """regex used to filter logs"""
@@ -240,7 +237,9 @@
                                look_for_keys=False)
         except paramiko.ssh_exception.AuthenticationException:
             if not password:
-                self.__ssh.get_transport().auth_none(username)
+                transport = self.__ssh.get_transport()
+                assert transport is not None
+                transport.auth_none(username)
             else:
                 raise
 
diff --git a/tools/otci/otci/connectors.py b/tools/otci/otci/connectors.py
index 713fbc3..55477e1 100644
--- a/tools/otci/otci/connectors.py
+++ b/tools/otci/otci/connectors.py
@@ -29,17 +29,16 @@
 import logging
 import subprocess
 import time
-from abc import abstractmethod
+from abc import abstractmethod, ABC
 from typing import Optional
 
 
-class OtCliHandler:
+class OtCliHandler(ABC):
     """This abstract class defines interfaces for a OT CLI Handler."""
 
     @abstractmethod
-    def readline(self) -> str:
+    def readline(self) -> Optional[str]:
         """Method readline should return the next line read from OT CLI."""
-        pass
 
     @abstractmethod
     def writeline(self, s: str) -> None:
@@ -47,7 +46,6 @@
 
         It should block until all characters are written to OT CLI.
         """
-        pass
 
     @abstractmethod
     def wait(self, duration: float) -> None:
@@ -56,15 +54,13 @@
         A normal implementation should just call `time.sleep(duration)`. This is intended for proceeding Virtual Time
         Simulation instances.
         """
-        pass
 
     @abstractmethod
     def close(self) -> None:
         """Method close should close the OT CLI Handler."""
-        pass
 
 
-class Simulator:
+class Simulator(ABC):
     """This abstract class defines interfaces for a Virtual Time Simulator."""
 
     @abstractmethod
@@ -84,10 +80,12 @@
     def __repr__(self):
         return 'OTCli<%d>' % self.__nodeid
 
-    def readline(self) -> str:
+    def readline(self) -> Optional[str]:
+        assert self.__otcli_proc.stdout is not None
         return self.__otcli_proc.stdout.readline().rstrip('\r\n')
 
     def writeline(self, s: str):
+        assert self.__otcli_proc.stdin is not None
         self.__otcli_proc.stdin.write(s + '\n')
         self.__otcli_proc.stdin.flush()
 
@@ -100,6 +98,8 @@
             time.sleep(duration)
 
     def close(self):
+        assert self.__otcli_proc.stdin is not None
+        assert self.__otcli_proc.stdout is not None
         self.__otcli_proc.stdin.close()
         self.__otcli_proc.stdout.close()
         self.__otcli_proc.wait()
@@ -120,7 +120,7 @@
         super().__init__(proc, nodeid, simulator)
 
 
-class OtNcpSim(OtCliHandler):
+class OtNcpSim(OtCliPopen):
     """Connector for OT NCP Simulation instances."""
 
     def __init__(self, executable: str, nodeid: int, simulator: Simulator):
diff --git a/tools/otci/otci/otci.py b/tools/otci/otci/otci.py
index b75ecc0..d9d5c1f 100644
--- a/tools/otci/otci/otci.py
+++ b/tools/otci/otci/otci.py
@@ -64,7 +64,7 @@
         """Gets the string representation of the OTCI instance."""
         return repr(self.__otcmd)
 
-    def wait(self, duration: float, expect_line: Union[str, Pattern, Collection[Any]] = None):
+    def wait(self, duration: float, expect_line: Optional[Union[str, Pattern, Collection[Any]]] = None):
         """Wait for a given duration.
 
         :param duration: The duration (in seconds) wait for.
@@ -104,6 +104,7 @@
                 self.wait(2)
                 if i == self.__exec_command_retry:
                     raise
+        assert False
 
     def __execute_command(self,
                           cmd: str,
@@ -219,7 +220,7 @@
     )
 
     def ping(self,
-             ip: str,
+             ip: Union[str, Ip6Addr],
              size: int = 8,
              count: int = 1,
              interval: float = 1,
@@ -261,15 +262,15 @@
         """Stop sending ICMPv6 Echo Requests."""
         self.execute_command('ping stop')
 
-    def discover(self, channel: int = None) -> List[Dict[str, Any]]:
+    def discover(self, channel: Optional[int] = None) -> List[Dict[str, Any]]:
         """Perform an MLE Discovery operation."""
         return self.__scan_networks('discover', channel)
 
-    def scan(self, channel: int = None) -> List[Dict[str, Any]]:
+    def scan(self, channel: Optional[int] = None) -> List[Dict[str, Any]]:
         """Perform an IEEE 802.15.4 Active Scan."""
         return self.__scan_networks('scan', channel)
 
-    def __scan_networks(self, cmd: str, channel: int = None) -> List[Dict[str, Any]]:
+    def __scan_networks(self, cmd: str, channel: Optional[int] = None) -> List[Dict[str, Any]]:
         if channel is not None:
             cmd += f' {channel}'
 
@@ -300,7 +301,7 @@
 
         return networks
 
-    def scan_energy(self, duration: float = None, channel: int = None) -> Dict[int, int]:
+    def scan_energy(self, duration: Optional[float] = None, channel: Optional[int] = None) -> Dict[int, int]:
         """Perform an IEEE 802.15.4 Energy Scan."""
         cmd = 'scan energy'
         if duration is not None:
@@ -496,9 +497,9 @@
         """Try to switch to state detached, child, router or leader."""
         self.execute_command(f'state {state}')
 
-    def get_rloc16(self) -> int:
+    def get_rloc16(self) -> Rloc16:
         """Get the Thread RLOC16 value."""
-        return self.__parse_int(self.execute_command('rloc16'), 16)
+        return Rloc16(self.__parse_int(self.execute_command('rloc16'), 16))
 
     def get_router_id(self) -> int:
         """Get the Thread Router ID value."""
@@ -784,7 +785,9 @@
         for line in output:
             k, v = line.split(': ')
             if k == 'Server':
-                ip, port = re.match(OTCI._IPV6_SERVER_PORT_PATTERN, v).groups()
+                matched = re.match(OTCI._IPV6_SERVER_PORT_PATTERN, v)
+                assert matched is not None
+                ip, port = matched.groups()
                 config['server'] = (Ip6Addr(ip), int(port))
             elif k == 'ResponseTimeout':
                 config['response_timeout'] = int(v[:-3])
@@ -799,9 +802,9 @@
 
     def dns_set_config(self,
                        server: Tuple[Union[str, ipaddress.IPv6Address], int],
-                       response_timeout: int = None,
-                       max_tx_attempts: int = None,
-                       recursion_desired: bool = None):
+                       response_timeout: Optional[int] = None,
+                       max_tx_attempts: Optional[int] = None,
+                       recursion_desired: Optional[bool] = None):
         """Set DNS client query config."""
         cmd = f'dns config {str(server[0])} {server[1]}'
         if response_timeout is not None:
@@ -936,6 +939,7 @@
                 info = {'host': line}
                 result.append(info)
             else:
+                assert info is not None
                 k, v = line.strip().split(': ')
                 if k == 'deleted':
                     if v not in ('true', 'false'):
@@ -962,6 +966,7 @@
                 info = {'instance': line}
                 result.append(info)
             else:
+                assert info is not None
                 k, v = line.strip().split(': ')
                 if k == 'deleted':
                     if v not in ('true', 'false'):
@@ -1130,7 +1135,7 @@
                                port: int,
                                priority: int = 0,
                                weight: int = 0,
-                               txt: Dict[str, Union[str, bytes, bool]] = None):
+                               txt: Optional[Dict[str, Union[str, bytes, bool]]] = None):
         instance = self.__escape_escapable(instance)
         cmd = f'srp client service add {instance} {service} {port} {priority} {weight}'
         if txt:
@@ -1164,7 +1169,9 @@
     def srp_client_get_server(self) -> Tuple[Ip6Addr, int]:
         """Get the SRP server (IP, port)."""
         result = self.__parse_str(self.execute_command('srp client server'))
-        ip, port = re.match(OTCI._IPV6_SERVER_PORT_PATTERN, result).groups()
+        matched = re.match(OTCI._IPV6_SERVER_PORT_PATTERN, result)
+        assert matched
+        ip, port = matched.groups()
         return Ip6Addr(ip), int(port)
 
     def srp_client_get_service_key(self) -> bool:
@@ -1375,15 +1382,19 @@
             if k == 'Channel':
                 cfg['channel'] = int(v)
             elif k == 'Timeout':
-                cfg['timeout'] = int(OTCI._CSL_TIMEOUT_PATTERN.match(v).group(1))
+                matched = OTCI._CSL_TIMEOUT_PATTERN.match(v)
+                assert matched is not None
+                cfg['timeout'] = int(matched.group(1))
             elif k == 'Period':
-                cfg['period'] = int(OTCI._CSL_PERIOD_PATTERN.match(v).group(1))
+                matched = OTCI._CSL_PERIOD_PATTERN.match(v)
+                assert matched is not None
+                cfg['period'] = int(matched.group(1))
             else:
                 logging.warning("Ignore unknown CSL parameter: %s: %s", k, v)
 
         return cfg
 
-    def config_csl(self, channel: int = None, period: int = None, timeout: int = None):
+    def config_csl(self, channel: Optional[int] = None, period: Optional[int] = None, timeout: Optional[int] = None):
         """Configure CSL parameters.
 
         :param channel: Set CSL channel.
@@ -1487,7 +1498,7 @@
     #
     # Joiner operations
     #
-    def joiner_start(self, psk: str, provisioning_url: str = None):
+    def joiner_start(self, psk: str, provisioning_url: Optional[str] = None):
         """Start the Joiner."""
         cmd = f'joiner start {psk}'
         if provisioning_url is not None:
@@ -1780,17 +1791,17 @@
         self.execute_command(cmd)
 
     def dataset_set_buffer(self,
-                           active_timestamp: int = None,
-                           channel: int = None,
-                           channel_mask: int = None,
-                           extpanid: str = None,
-                           mesh_local_prefix: str = None,
-                           network_key: str = None,
-                           network_name: str = None,
-                           panid: int = None,
-                           pskc: str = None,
-                           security_policy: tuple = None,
-                           pending_timestamp: int = None):
+                           active_timestamp: Optional[int] = None,
+                           channel: Optional[int] = None,
+                           channel_mask: Optional[int] = None,
+                           extpanid: Optional[str] = None,
+                           mesh_local_prefix: Optional[str] = None,
+                           network_key: Optional[str] = None,
+                           network_name: Optional[str] = None,
+                           panid: Optional[int] = None,
+                           pskc: Optional[str] = None,
+                           security_policy: Optional[tuple] = None,
+                           pending_timestamp: Optional[int] = None):
         if active_timestamp is not None:
             self.execute_command(f'dataset activetimestamp {active_timestamp}')
 
@@ -1840,7 +1851,7 @@
     def disable_allowlist(self):
         self.execute_command('macfilter addr disable')
 
-    def add_allowlist(self, addr: str, rssi: int = None):
+    def add_allowlist(self, addr: str, rssi: Optional[int] = None):
         cmd = f'macfilter addr add {addr}'
 
         if rssi is not None:
@@ -1964,14 +1975,14 @@
     def enable_backbone_router(self):
         """Enable Backbone Router Service for Thread 1.2 FTD.
 
-        SRV_DATA.ntf would be triggerred for attached device if there is no Backbone Router Service in Thread Network Data.
+        SRV_DATA.ntf would be triggered for attached device if there is no Backbone Router Service in Thread Network Data.
         """
         self.execute_command('bbr enable')
 
     def disable_backbone_router(self):
         """Disable Backbone Router Service for Thread 1.2 FTD.
 
-        SRV_DATA.ntf would be triggerred if Backbone Router is Primary state.
+        SRV_DATA.ntf would be triggered if Backbone Router is Primary state.
         """
         self.execute_command('bbr disable')
 
@@ -2025,7 +2036,7 @@
     def register_backbone_router_dataset(self):
         """Register Backbone Router Service for Thread 1.2 FTD.
 
-        SRV_DATA.ntf would be triggerred for attached device.
+        SRV_DATA.ntf would be triggered for attached device.
         """
         self.execute_command('bbr register')
 
@@ -2053,7 +2064,10 @@
 
         return config
 
-    def set_backbone_router_config(self, seqno: int = None, delay: int = None, timeout: int = None):
+    def set_backbone_router_config(self,
+                                   seqno: Optional[int] = None,
+                                   delay: Optional[int] = None,
+                                   timeout: Optional[int] = None):
         """Configure local Backbone Router configuration for Thread 1.2 FTD.
 
         Call register_backbone_router_dataset() to explicitly register Backbone Router service to Leader for Secondary Backbone Router.
@@ -2217,7 +2231,12 @@
         """
         self.execute_command(f'udp connect {ip} {port}')
 
-    def udp_send(self, ip: str = None, port: int = None, text: str = None, random_bytes: int = None, hex: str = None):
+    def udp_send(self,
+                 ip: Optional[Union[str, Ip6Addr]] = None,
+                 port: Optional[int] = None,
+                 text: Optional[str] = None,
+                 random_bytes: Optional[int] = None,
+                 hex: Optional[str] = None):
         """Send a few bytes over UDP.
 
         ip: the IPv6 destination address.
@@ -2292,11 +2311,11 @@
         """Stops the application coap service."""
         self.execute_command('coap stop')
 
-    def coap_get(self, addr: str, uri_path: str, type: str = "con"):
+    def coap_get(self, addr: Union[str, Ip6Addr], uri_path: str, type: str = "con"):
         cmd = f'coap get {addr} {uri_path} {type}'
         self.execute_command(cmd)
 
-    def coap_put(self, addr: str, uri_path: str, type: str = "con", payload: str = None):
+    def coap_put(self, addr: Union[str, Ip6Addr], uri_path: str, type: str = "con", payload: Optional[str] = None):
         cmd = f'coap put {addr} {uri_path} {type}'
 
         if payload is not None:
@@ -2304,7 +2323,7 @@
 
         self.execute_command(cmd)
 
-    def coap_post(self, addr: str, uri_path: str, type: str = "con", payload: str = None):
+    def coap_post(self, addr: Union[str, Ip6Addr], uri_path: str, type: str = "con", payload: Optional[str] = None):
         cmd = f'coap post {addr} {uri_path} {type}'
 
         if payload is not None:
@@ -2312,7 +2331,7 @@
 
         self.execute_command(cmd)
 
-    def coap_delete(self, addr: str, uri_path: str, type: str = "con", payload: str = None):
+    def coap_delete(self, addr: Union[str, Ip6Addr], uri_path: str, type: str = "con", payload: Optional[str] = None):
         cmd = f'coap delete {addr} {uri_path} {type}'
 
         if payload is not None:
diff --git a/tools/otci/otci/types.py b/tools/otci/otci/types.py
index 2229552..d1e9d11 100644
--- a/tools/otci/otci/types.py
+++ b/tools/otci/otci/types.py
@@ -28,6 +28,7 @@
 #
 import ipaddress
 from collections import namedtuple
+from enum import IntEnum
 
 
 class ChildId(int):
@@ -52,7 +53,7 @@
     pass
 
 
-class NetifIdentifier(int):
+class NetifIdentifier(IntEnum):
     """Represents a network interface identifier."""
     UNSPECIFIED = 0
     THERAD = 1
diff --git a/tools/otci/otci/utils.py b/tools/otci/otci/utils.py
index 3a52a82..b5952b5 100644
--- a/tools/otci/otci/utils.py
+++ b/tools/otci/otci/utils.py
@@ -33,7 +33,7 @@
 def match_line(line: str, expect_line: Union[str, Pattern, Collection[Any]]) -> bool:
     """Checks if a line is expected (matched by one of the given patterns)."""
     if isinstance(expect_line, Pattern):
-        match = expect_line.match(line)
+        match = expect_line.match(line) is not None
     elif isinstance(expect_line, str):
         match = (line == expect_line)
     else:
diff --git a/tools/spi-hdlc-adapter/spi-hdlc-adapter.c b/tools/spi-hdlc-adapter/spi-hdlc-adapter.c
index 0adff6b..48c256c 100644
--- a/tools/spi-hdlc-adapter/spi-hdlc-adapter.c
+++ b/tools/spi-hdlc-adapter/spi-hdlc-adapter.c
@@ -1416,7 +1416,7 @@
                        "    --spi-mode[=mode] ............ Specify the SPI mode to use (0-3).\n"
                        "    --spi-speed[=hertz] .......... Specify the SPI speed in hertz.\n"
                        "    --spi-cs-delay[=usec] ........ Specify the delay after C̅S̅ assertion, in µsec\n"
-                       "    --spi-reset-delay[=ms] ....... Specify the delay after R̅E̅S̅E̅T̅ assertion, in miliseconds\n"
+                       "    --spi-reset-delay[=ms] ....... Specify the delay after R̅E̅S̅E̅T̅ assertion, in milliseconds\n"
                        "    --spi-align-allowance[=n] .... Specify the maximum number of 0xFF bytes to\n"
                        "                                   clip from start of MISO frame. Max value is 16.\n"
                        "    --spi-small-packet=[n] ....... Specify the smallest packet we can receive\n"